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 §
- Git Flow:
main(production),develop(integration),feature/*,release/*,hotfix/*. Suited to release-driven projects. - Trunk-based: everyone commits to
main; feature flags control rollout. Short-lived branches ( day). - GitHub Flow:
main+ short-lived feature branches; pull requests for review.
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 §
- Review time: median time from PR opened to first review. Target: hours for small PRs.
- Defect escape rate: fraction of bugs found in production that should have been caught in review.
- PR size: median lines changed per PR. Smaller PRs ( lines) are reviewed more thoroughly.
Practical guidelines §
- Review within one business day; stale reviews lose context.
- Keep PRs small: one logical change per PR.
- Use checklists for recurring concerns (error handling, logging, API compatibility).
- Automate what can be automated: linters, formatters, and static analysis run in CI before a human reviews.
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.
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 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
- 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.
- 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.
- Keep only 5–10 E2E tests for the critical user journeys (login, checkout, payment). Target: E2E suite under 10 minutes, 0% flake rate.
- 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.
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: two identical environments; switch traffic from blue to green. Instant rollback: switch back.
- Canary: deploy to a small percentage of users (e.g., 5%); monitor metrics; roll out to 100% or roll back.
- Rolling update: replace instances one at a time (Kubernetes default). No extra capacity needed.
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:
- Deployment Frequency: how often code reaches production (per day, per week).
- Lead Time for Changes: time from commit to production.
- Change Failure Rate: fraction of deployments that cause an incident.
- Failed Deployment Recovery Time: time to restore service after a failed deployment.
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:
- CI pipeline: every commit triggers build + unit tests + integration tests. Catches most bugs before they reach production.
- Code review: every PR is reviewed before merge. A second pair of eyes catches logic errors that tests miss.
- Staging environment: deploy to a staging environment that mirrors production. Run acceptance tests and smoke tests.
- Monitoring and alerting: set up metrics (error rate, latency, throughput) and alerts. A spike in 500 errors triggers an alert within minutes, not days.
- 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:
- Rollback: keep the previous deployment artifact. Roll back in minutes, not hours.
- Feature flags: wrap risky features in flags. If a bug is found, disable the flag without redeploying.
- Canary deployment: deploy to 5% of users first. If the error rate spikes, roll back the canary before it affects all users.
- 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:
- Git Flow:
main(production),develop(integration),feature/*,release/*,hotfix/*. - Trunk-based: everyone commits to
main; feature flags control rollout. Short-lived branches ( day). - GitHub Flow:
main+ short-lived feature branches; PRs for review.
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:
- Blue-green: two identical environments; switch traffic from blue to green.
- Canary: deploy to a small percentage of users; monitor; roll out or roll back.
- Rolling update: replace instances one at a time (Kubernetes default).
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:
- CI pipeline: every commit triggers build + unit tests + integration tests. Catches most bugs before they reach production.
- Staging environment: deploy to a staging environment that mirrors production. Run acceptance tests and smoke tests.
- Monitoring and alerting: set up metrics (error rate, latency, throughput) and alerts. A spike in 500 errors triggers an alert within minutes, not days.
- 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:
- Rollback: keep the previous deployment artifact. Roll back in minutes, not hours.
- Feature flags: wrap risky features in flags. If a bug is found, disable the flag without redeploying.
- Canary deployment: deploy to 5% of users first. If the error rate spikes, roll back the canary before it affects all users.
- 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).