Technopedia

The technologist’s encyclopedia

Software Engineering

Software engineering studies how to build, test, deploy, and monitor software at scale. It is relevant whenever a program must be maintained by a team, deployed to production, and kept reliable over time. The discipline spans the full lifecycle: from committing code to a shared repository, through automated testing and continuous integration, to deploying with minimal risk and observing the running system.

Authoritative sources

Version control

Git is a distributed version control system. A commit is a snapshot of the repository with a message, author, and parent pointer. A branch is a movable pointer to a commit. A merge combines two branches; a rebase replays commits on top of another branch.

Details

The data model

Git stores four object types in a content-addressable store: blobs (file contents), trees (directory listings), commits (snapshots), and tags (named references). Each object is identified by a content-addressable hash (SHA-1 for existing repositories; Git 3.0 transitions new repositories to SHA-256). A commit points to a tree (the file system at that point) and one or more parent commits.

Example: You edit main.c and run git commit. Git creates a new blob for the file content, a new tree referencing that blob, and a new commit pointing to the tree and the previous commit. The branch pointer main advances to the new commit.

Branching strategies

Example: In a trunk-based team, Alice creates branch feat/login, commits three times, opens a pull request, and merges within a day. The branch is deleted. No long-lived integration branch exists.

Merge vs. rebase

Merge preserves history (a merge commit shows the divergence); rebase creates a linear history (rewrites commit hashes). Rebase is preferred for local commits; merge for shared branches.

Example problem

You have three local commits on branch feature/x that were created before a teammate pushed to main. Should you merge or rebase?

Solution

Rebase. Your commits are local (not shared), so rewriting their hashes is safe. Rebasing feature/x onto the updated main gives a linear history: your three commits appear as if they were made after the teammate’s work. If you had already pushed feature/x and others were building on it, you would merge instead to avoid rewriting shared history.

Code review

Code review is the practice of a peer examining a change (typically a pull request or merge request) before it is merged. It catches bugs, enforces style, transfers knowledge, and creates a second set of eyes on critical logic.

Details

What a review covers

A review examines: correctness (does the code do what the spec says?), readability (can another engineer follow it?), test coverage (are the changes tested?), and security (no injection, no secret leakage, no privilege escalation). See Security for the specific threat classes a reviewer should check.

Example: A pull request adds a new API endpoint. The reviewer checks: (1) input validation (no SQL injection), (2) error handling (no stack traces leaked to the client), (3) tests (a unit test for the happy path and one for a malformed input), (4) naming (the function name matches its behavior).

Review metrics

Practical guidelines

Example problem

A team notices that 40% of production bugs are traced to missing input validation. How should they change their review process?

Solution

Add a mandatory checklist item: “Does every external input (HTTP parameter, file, message) have explicit validation before use?” Pair this with an automated static analysis rule (e.g., a linter that flags unvalidated user input). The reviewer checks the automated findings; the linter catches the mechanical misses. Over time, the defect escape rate for injection-class bugs drops because the check is no longer dependent on individual reviewer attention.

Testing

Unit tests: test a single function or class in isolation. Integration tests: test the interaction between components. Acceptance tests: test the system against user requirements.

Assertion: a check that a condition holds; if it fails, the test fails. Regression tests: tests that verify a previously fixed bug does not reappear. Fuzzing: generate random or structured inputs to find crashes and undefined behavior.

Details

The test pyramid

The test pyramid organizes tests by cost and coverage. Many unit tests (fast, cheap, isolate logic), fewer integration tests (slower, test component interaction), few acceptance tests (slowest, test the full user journey). An inverted pyramid (many E2E tests) is fragile and slow.

Acceptance (few) Integration (some) Unit (many)

Example: A payment service has 200 unit tests (each testing one function: calculateTax, validateCard, formatInvoice), 30 integration tests (testing the payment flow: card → gateway → ledger), and 5 acceptance tests (end-to-end: user clicks “Pay” → confirmation email arrives).

Property-based testing

Instead of specific inputs, specify properties that must hold for all inputs. for all lists l: sort(sort(l)) == sort(l). Tools: QuickCheck (Haskell), Hypothesis (Python), proptest (Rust).

Example: You write a function merge(a, b) that merges two sorted lists. A property test asserts: for all sorted lists a, b: is_sorted(merge(a, b)) and for all sorted lists a, b: set(merge(a, b)) == set(a) ∪ set(b). The test framework generates hundreds of random inputs and checks the property holds for each.

Fuzzing

Coverage-guided fuzzing (AFL, libFuzzer) uses coverage feedback to guide input generation. Structure-aware fuzzing (Jazzer, honggfuzz) uses the program’s type information to generate valid inputs.

Example: You fuzz a JSON parser. The fuzzer starts with {"key": "value"}, mutates bytes, and tracks which code paths are hit. When a new branch is reached (e.g., a nested object), the fuzzer focuses mutations there. After 10 million iterations, it finds a 3-byte input that triggers a buffer overflow.

Mutation testing

Introduce small bugs (mutations) into the code; if the test suite does not catch the mutation, the tests are insufficient.

Example: The line if (x > 0) is mutated to if (x >= 0). If no test fails, the suite does not distinguish between the two, meaning the boundary case x=0x = 0 is untested.

Authoritative sources

Example problem

Your team has 500 unit tests and 50 end-to-end tests. The end-to-end suite takes 45 minutes and is flaky (fails 10% of the time on CI). What should you do?

Solution
  1. Identify the flaky E2E tests: usually they depend on timing, shared state, or external services. Replace them with integration tests that mock the external dependency.
  2. Move assertions from E2E to unit/integration level: if an E2E test checks that “the total is correct”, add a unit test for the calculation and an integration test for the API response.
  3. Keep only 5–10 E2E tests for the critical user journeys (login, checkout, payment). Target: E2E suite under 10 minutes, 0% flake rate.
  4. The freed CI time (35 minutes) can be spent on more integration tests or a longer fuzzing run.

CI/CD

CI (Continuous Integration): every commit triggers a build, test, and static analysis pipeline. Fail fast: the first failing stage stops the pipeline.

CD (Continuous Delivery/Deployment): every passing build is deployable. Continuous Delivery: a human approves the deployment. Continuous Deployment: the deployment is automatic.

The DORA metrics (Deployment Frequency, Lead Time for Changes, Change Failure Rate, Failed Deployment Recovery Time) are the industry-standard framework for measuring software delivery performance. They are tracked in the observability stack and reviewed in team retrospectives.

Details

Pipeline stages

A typical pipeline: build → unit test → integration test → security scan → deploy to staging → acceptance test → deploy to production. The security scan stage (SAST, dependency audit, container image scan) is described in Security.

Build Unit test Integration Security scan Staging Production

Example: A developer pushes a commit. The CI runner compiles the code (build), runs 200 unit tests (30 s), runs 30 integration tests against a test database (2 min), scans dependencies for known CVEs (1 min), and deploys to a staging environment. If the integration tests pass, the staging deployment triggers 5 acceptance tests. All green → the artifact is tagged v2.14.3 and is ready for production.

Deployment strategies

Blue-Green Blue (old) Green (new) Switch LB Canary 95% 5% Monitor → 100% (or rollback) Rolling One at a time

Example: A canary deployment of a new search algorithm: 5% of traffic (≈ 500 requests/min) hits the new version. After 10 minutes, the error rate is 0.2% (baseline 0.1%) and p99 latency is 120 ms (baseline 110 ms). The team rolls out to 25%, then 50%, then 100% over the next hour. If at any step the error rate exceeds 1%, the canary is rolled back automatically.

Infrastructure as Code

Define infrastructure (servers, networks, databases) in code (Terraform, Pulumi, CloudFormation). Version-controlled, reviewable, reproducible. The target Operating System and Computer Hardware are declared in the IaC configuration, so the environment is identical from staging to production.

Example: A Terraform file declares: 3 t3.medium instances in an auto-scaling group, an ALB in front, an RDS PostgreSQL instance, and a VPC with public/private subnets. Running terraform apply creates or updates all of these. The same file is used for staging (1 instance) and production (3 instances) via a variable.

DORA metrics in practice

The four DORA metrics are computed from CI/CD and observability data:

Teams are categorized as Elite, High, Medium, or Low performers based on these four metrics. The goal is not a specific number but continuous improvement: reduce lead time, increase deployment frequency, and keep failure rate and recovery time low.

Example problem

Your team deploys once a month. Each deployment takes 4 hours and has a 30% failure rate. When a deployment fails, it takes 2 days to recover. Using DORA metrics, what is your current performance level, and what is the first change you would make?

Solution

Current DORA metrics: Deployment Frequency = 1/month (Low), Lead Time = unknown but likely days (Low), Change Failure Rate = 30% (Low), Recovery Time = 2 days (Low). This is a “Low” performer.

First change: break the monthly deployment into smaller, more frequent deployments. Introduce a CI pipeline so that every commit is built and tested. Deploy to staging automatically. This reduces the blast radius of each deployment (smaller change = lower failure rate) and makes failures easier to diagnose (you know which commit broke it). Target: weekly deployments within one quarter, daily within two quarters.

Observability

Logging: structured records of events (timestamp, level, message, context). Metrics: numerical time series (CPU, memory, request rate, latency). Tracing: distributed traces that follow a request across services (OpenTelemetry, Jaeger). In a Distributed Systems architecture, tracing is essential to follow a request across service boundaries.

Alerting: notify when a metric exceeds a threshold or a pattern is detected. SLO (Service Level Objective): a target (e.g., 99.9% of requests < 200 ms). Error budget: the allowed failure rate (0.1%); when exhausted, freeze feature work and fix reliability.

Details

The three pillars

Logs (what happened), metrics (how much), traces (how it flowed). Together, they answer: “Is the system healthy? What is wrong? Where did it fail?”

Example: A user reports a slow checkout. The metrics dashboard shows p99 latency spiked at 14:02. The trace for that request shows: api-gateway (5 ms) → auth-service (12 ms) → inventory-service (1800 ms) → database (1750 ms). The logs for inventory-service at 14:02 show: WARN: connection pool exhausted, waiting 1750 ms for a connection. Root cause: the connection pool was too small for the traffic spike.

OpenTelemetry

A vendor-neutral standard for instrumentation. The SDK collects spans, metrics, and logs; the collector exports them to a backend (Jaeger, Prometheus, Loki). The specification defines the data model (spans, attributes, resource) and the protocol (OTLP over gRPC or HTTP).

Example: A Go service uses the OpenTelemetry SDK to create a span for each HTTP request. The span includes attributes: http.method, http.status_code, db.system=postgres. The OTLP collector batches spans and exports them to Jaeger. In the Jaeger UI, you search by service=inventory-service and http.status_code=500 to find all failing requests.

Dashboards and correlation

Grafana (metrics), Kibana (logs), Jaeger (traces). Correlate by trace ID: a slow request in the dashboard links to the trace, which links to the logs. A single trace ID threads through all three systems.

Chaos engineering

Intentionally inject failures (kill a node, add latency, drop packets) to verify that the system behaves as expected. This is a practical application of Fault Tolerance principles: you test the system’s resilience before a real failure occurs. Tools: Chaos Monkey (Netflix), Litmus (Kubernetes).

Example: Every Friday at 10:00, a chaos experiment kills one pod in the payment-service deployment. The expected behavior: the load balancer routes to the remaining pods, the killed pod is restarted by the orchestrator, and the error rate stays below 0.1%. If the error rate spikes above 1%, the experiment is a failure and the team investigates before the next run.

Authoritative sources

Example problem

Your SLO is 99.9% of requests < 500 ms. Over the last 30 days, 0.3% of requests exceeded 500 ms. What is your error budget consumption, and what should you do?

Solution

Error budget = 1 − 0.999 = 0.1% (1 in 1000 requests may exceed 500 ms). You consumed 0.3%, which is 3× your budget. The budget is exhausted.

Action: freeze feature development for one sprint. Use the sprint to: (1) identify the slow requests via tracing (which service, which query), (2) fix the root cause (add an index, increase connection pool, add caching), (3) verify the SLO is met for 7 consecutive days before resuming feature work. This is the SLO-driven development loop: the error budget is the mechanism that forces reliability work to compete with feature work on equal footing.

Interactive simulation

Quiz

Example problem

Problem: A team of 5 developers works on a web application. They deploy weekly, and each deployment takes 2 hours. A bug is found in production 3 days after deployment. How can they reduce the time-to-detection and time-to-recovery?

Solution

Reduce time-to-detection:

  1. CI pipeline: every commit triggers build + unit tests + integration tests. Catches most bugs before they reach production.
  2. Code review: every PR is reviewed before merge. A second pair of eyes catches logic errors that tests miss.
  3. Staging environment: deploy to a staging environment that mirrors production. Run acceptance tests and smoke tests.
  4. Monitoring and alerting: set up metrics (error rate, latency, throughput) and alerts. A spike in 500 errors triggers an alert within minutes, not days.
  5. Structured logging: log request IDs, user IDs, and error details. When an alert fires, the on-call engineer can find the relevant logs in seconds.

Reduce time-to-recovery:

  1. Rollback: keep the previous deployment artifact. Roll back in minutes, not hours.
  2. Feature flags: wrap risky features in flags. If a bug is found, disable the flag without redeploying.
  3. Canary deployment: deploy to 5% of users first. If the error rate spikes, roll back the canary before it affects all users.
  4. Blue-green: keep the old environment running. Switch traffic back if the new deployment fails.

Result: weekly 2-hour deployments become daily 10-minute deployments. Bugs are caught in CI (minutes) or in the canary (minutes), not in production (days). DORA metrics improve: Deployment Frequency goes from 1/week to 1/day; Failed Deployment Recovery Time goes from 3 days to minutes.

Software engineering studies how to build, test, deploy, and monitor software at scale. It is relevant whenever a program must be maintained by a team, deployed to production, and kept reliable over time.

Authoritative sources

Version control

Git is a distributed version control system. A commit is a snapshot of the repository with a message, author, and parent pointer. A branch is a movable pointer to a commit. A merge combines two branches; a rebase replays commits on top of another branch.

Details

Git data model: objects (blobs, trees, commits, tags) stored in a content-addressable store (SHA-1 hash). A commit points to a tree (the file system at that point) and one or more parent commits.

Branching strategies:

Merge vs. rebase: merge preserves history (a merge commit shows the divergence); rebase creates a linear history (rewrites commit hashes). Rebase is preferred for local commits; merge for shared branches.

Testing

Unit tests: test a single function or class in isolation. Integration tests: test the interaction between components. Acceptance tests: test the system against user requirements.

Assertion: a check that a condition holds; if it fails, the test fails. Regression tests: tests that verify a previously fixed bug does not reappear. Fuzzing: generate random or structured inputs to find crashes and undefined behavior.

Details

Test pyramid: many unit tests (fast, cheap), fewer integration tests (slower), few acceptance tests (slowest). Inverted pyramid (many E2E tests) is fragile and slow.

Property-based testing: instead of specific inputs, specify properties that must hold for all inputs. for all lists l: sort(sort(l)) == sort(l). Tools: QuickCheck (Haskell), Hypothesis (Python), proptest (Rust).

Fuzzing: coverage-guided fuzzing (AFL, libFuzzer) uses coverage feedback to guide input generation. Structure-aware fuzzing (Jazzer, honggfuzz) uses the program’s type information to generate valid inputs.

Mutation testing: introduce small bugs (mutations) into the code; if the test suite does not catch the mutation, the tests are insufficient.

CI/CD

CI (Continuous Integration): every commit triggers a build, test, and static analysis pipeline. Fail fast: the first failing stage stops the pipeline.

CD (Continuous Delivery/Deployment): every passing build is deployable. Continuous Delivery: a human approves the deployment. Continuous Deployment: the deployment is automatic.

Details

Pipeline stages: build → unit test → integration test → security scan → deploy to staging → acceptance test → deploy to production.

Deployment strategies:

Infrastructure as Code (IaC): define infrastructure (servers, networks, databases) in code (Terraform, Pulumi, CloudFormation). Version-controlled, reviewable, reproducible.

Observability

Logging: structured records of events (timestamp, level, message, context). Metrics: numerical time series (CPU, memory, request rate, latency). Tracing: distributed traces that follow a request across services (OpenTelemetry, Jaeger).

Alerting: notify when a metric exceeds a threshold or a pattern is detected. SLO (Service Level Objective): a target (e.g., 99.9% of requests < 200 ms). Error budget: the allowed failure rate (0.1%); when exhausted, freeze feature work and fix reliability.

Details

The three pillars: logs (what happened), metrics (how much), traces (how it flowed). Together, they answer: “Is the system healthy? What is wrong? Where did it fail?”

OpenTelemetry: a vendor-neutral standard for instrumentation. The SDK collects spans, metrics, and logs; the collector exports them to a backend (Jaeger, Prometheus, Loki).

Dashboards: Grafana (metrics), Kibana (logs), Jaeger (traces). Correlate by trace ID: a slow request in the dashboard links to the trace, which links to the logs.

Chaos engineering: intentionally inject failures (kill a node, add latency, drop packets) to verify that the system behaves as expected. Tools: Chaos Monkey (Netflix), Litmus (Kubernetes).

Example problem

Problem: A team of 5 developers works on a web application. They deploy weekly, and each deployment takes 2 hours. A bug is found in production 3 days after deployment. How can they reduce the time-to-detection and time-to-recovery?

Solution

Reduce time-to-detection:

  1. CI pipeline: every commit triggers build + unit tests + integration tests. Catches most bugs before they reach production.
  2. Staging environment: deploy to a staging environment that mirrors production. Run acceptance tests and smoke tests.
  3. Monitoring and alerting: set up metrics (error rate, latency, throughput) and alerts. A spike in 500 errors triggers an alert within minutes, not days.
  4. Structured logging: log request IDs, user IDs, and error details. When an alert fires, the on-call engineer can find the relevant logs in seconds.

Reduce time-to-recovery:

  1. Rollback: keep the previous deployment artifact. Roll back in minutes, not hours.
  2. Feature flags: wrap risky features in flags. If a bug is found, disable the flag without redeploying.
  3. Canary deployment: deploy to 5% of users first. If the error rate spikes, roll back the canary before it affects all users.
  4. Blue-green: keep the old environment running. Switch traffic back if the new deployment fails.

Result: weekly 2-hour deployments become daily 10-minute deployments. Bugs are caught in CI (minutes) or in the canary (minutes), not in production (days).