system-design

CI/CD Pipelines

The assembly line from a commit to production. Done well, deploying is a non-event that happens fifty times a day. Done badly, it’s a terrifying quarterly ritual.

Prerequisites: Containers Time to read: ~16 minutes


The problem

Code that isn’t deployed provides no value. But deploying is risky — bugs, broken environments, manual mistakes. The historical response was to deploy rarely (once a quarter, a big scary event), which paradoxically made it more dangerous: huge batches of changes, so when something broke, you had no idea which of 500 changes caused it.

🚨 The counter-intuitive insight: deploying more often is safer, not riskier. Small, frequent deploys mean each change is tiny, easy to review, easy to test, and — if it breaks — easy to identify and roll back. CI/CD is the automation that makes frequent, safe deployment possible.

Deploy once a quarter:  500 changes at once → something breaks → which one? → hours to find
Deploy 50× a day:       1 change → it breaks → obviously that change → roll back in 2 min

CI vs CD

Three terms, often blurred:

🚨 The distinction that matters: continuous delivery keeps a human in the loop for the production push; continuous deployment removes it entirely. Which you want depends on your confidence in your tests and your risk tolerance — a payment system might keep the human gate; a content site might fully automate.


The pipeline stages

flowchart LR
    C[Commit] --> B[Build]
    B --> T[Test]
    T --> S[Security scan]
    S --> A[Build artifact/image]
    A --> ST[Deploy to staging]
    ST --> IT[Integration tests]
    IT --> P[Deploy to prod]
    P --> V[Verify/monitor]

Build — compile, resolve dependencies, produce an artifact. 🚨 Build the deployable artifact once (a container image) and promote that same artifact through every environment — never rebuild per environment, or you lose the guarantee that what you tested is what you ship.

Test — the critical stage, in a pyramid:

Security scan — dependency vulnerability scanning (npm audit, Snyk), secret scanning (catch committed secrets → Secrets), static analysis (SAST), container image scanning. → OWASP

Artifact — the immutable, versioned output, stored in a registry.

Deploy — to staging, then (with a gate or automatically) to production, using a safe deployment strategy (rolling, canary, blue-green).

Verify — 🚨 the stage people forget: after deploy, automatically check it’s healthy (smoke tests, monitoring) and auto-rollback if not. A deploy isn’t done when the code is out; it’s done when you’ve confirmed it works.


What makes a pipeline good

🚨 These principles come up, and they’re what separate a pipeline that helps from one that hurts:

1. Fast. A slow pipeline (>10-15 min) breaks the feedback loop — developers stop waiting for it and lose trust. Parallelize, cache, fail fast.

2. Reliable / deterministic. 🚨 Flaky tests are poison — a test that fails randomly trains people to re-run and ignore failures, so a real failure gets ignored too (the same dynamic as alert fatigue). Fix or quarantine flaky tests aggressively.

3. Automated end to end. Every manual step is a place for human error and a bottleneck. The goal is that a merge to main can reach production with no manual intervention (whether it does automatically is the delivery-vs-deployment choice).

4. Reproducible. The same commit produces the same result every time — pinned dependencies, hermetic builds, no “it worked yesterday.”

5. Fast rollback. 🚨 The most important safety property. When a deploy goes bad, you need to undo it in seconds — because mitigate-before-diagnose means rolling back is your first move. A pipeline that can deploy fast but not roll back fast is dangerous.

6. Observable. You can see the state of every deploy and its history.


The deployment safety net

CI/CD pairs with several practices to make frequent deploys safe:

🚨 Together, these make a bad deploy a non-event: it hits 1% of users (canary), the flag is off for most, monitoring catches it, and it rolls back automatically. That’s the point — not preventing all bad deploys (impossible), but making them cheap.


Trunk-based development

🚨 A practice that goes hand-in-hand with CI/CD, and a good thing to know:

Instead of long-lived feature branches that diverge for weeks (and produce painful “merge hell”), developers integrate small changes into the main branch (trunk) frequently — at least daily.

✅ Small changes, continuous integration (the “CI” is real), no giant merges, always-deployable trunk. ❌ Requires discipline: feature flags to hide incomplete work, and a fast, reliable pipeline to keep trunk healthy.

This contrasts with heavy branching models (GitFlow) that batch changes into big releases — the opposite of what CI/CD wants. Small, frequent, integrated changes are the whole philosophy.


Pipeline security

🚨 The CI/CD pipeline is a high-value attack target — it has access to production and can inject code into everything you ship (supply chain, SolarWinds):


⚖️ Trade-offs

Choice Gain Cost
Frequent small deploys Safer, easier to diagnose, faster feedback Requires automation and discipline
Continuous deployment (no gate) Fastest delivery Requires very high test confidence
Continuous delivery (human gate) Human judgment on timing Slower; a manual step
Comprehensive test pyramid Catches bugs before prod Test maintenance cost; slower pipeline if unbalanced
Trunk-based development Continuous integration, no merge hell Needs feature flags and discipline

In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Build a real pipeline. Set up GitHub Actions (or GitLab CI) for a small project: on push, build, run tests, build a container image, push it to a registry. Then push a commit that breaks a test and watch the pipeline stop it before it ships. That gate — catching a bug automatically at commit time — is the whole value.

2. Feel the fast-feedback problem. Add a deliberately slow test (a 5-minute sleep) to your pipeline. Notice how you stop waiting for it and context-switch. Then parallelize/cache to bring it back under 2 minutes. The difference in how much you use the pipeline is real.

3. Practice rollback. Deploy something, then deploy a broken version. Time how long it takes to roll back. If it’s slow or manual, that’s your safety gap. Automate it (redeploy the previous image) and re-time.

4. Introduce a flaky test. Add a test that fails randomly 20% of the time. Run the pipeline several times and feel the instinct to just “re-run it.” That instinct is exactly how a real failure gets ignored — which is why flaky tests must be eliminated.


Check yourself

1. Why are frequent, small deployments safer than infrequent, large ones? Because they shrink the blast radius and the diagnostic problem of each deploy. A quarterly release bundles hundreds of changes, so when something breaks, you can't tell which of the 500 changes caused it — hours of investigation — and rolling back means reverting everyone's work. A deploy containing a single small change is trivially diagnosable (if it breaks, it's obviously that change), trivially reviewable, and trivially reversible (roll back one change in minutes). Small changes are also less likely to break in the first place — less code, fewer interactions, easier to test thoroughly. Counter-intuitively, the DORA research confirms that teams deploying many times per day have *lower* change-failure rates and faster recovery than teams deploying rarely. The historical instinct to deploy rarely "to be safe" actually made deployment more dangerous by batching risk; CI/CD inverts this by making frequent, small, automated deployment the safe path.
2. What's the difference between continuous delivery and continuous deployment? Both automate everything up to production, but they differ on the final production push. **Continuous delivery** keeps a human in the loop: every change that passes all automated tests is built, verified, and made *ready* to deploy, but a person presses the button to actually release it to production, deciding *when*. **Continuous deployment** removes that human gate entirely: every change that passes all tests deploys to production automatically, with no manual intervention. The choice depends on confidence and risk tolerance — continuous deployment requires very high trust in your automated tests and safety mechanisms (since nothing else stands between a merge and production), so it suits systems where the cost of a bad deploy is low and easily reversible, while continuous delivery's human gate suits higher-stakes systems (payments) where you want deliberate timing and a final human judgment. Both are "CD"; the distinction is whether a human approves the production release.
3. Why should you build the deployable artifact once and promote it? To guarantee that what you tested is exactly what you ship. If you build a fresh artifact for each environment — one for staging, another for production — then subtle differences can creep in between builds (a dependency version resolved differently, a changed base image, a build-time variable, a non-deterministic step), meaning the artifact you validated in staging is *not* the one running in production. That gap is where "it passed staging but broke in production" bugs live. Building the artifact once (typically an immutable, versioned container image) and promoting that same bit-for-bit image through staging, integration testing, and production means every environment runs identical code, so passing tests in staging genuinely predicts production behaviour. It also makes rollback trivial (the previous image still exists) and builds faster (no rebuild per stage). Build once, promote the same artifact everywhere.
4. Why is fast rollback the most important pipeline safety property? Because rollback is your first response when a deploy goes wrong, and incident response depends on recovering fast. The principle of "mitigate before you diagnose" means that when a deploy breaks production, you don't spend time understanding the bug while users suffer — you undo the change to restore service immediately, *then* investigate at leisure. If your pipeline can deploy quickly but rolling back is slow, manual, or risky (doesn't handle a schema change, requires a full rebuild), then you've lost your fastest, safest mitigation exactly when you need it, and the outage drags on while you either debug live or fumble a slow rollback. Fast, reliable, one-command rollback (redeploy the previous immutable artifact) turns a bad deploy from a crisis into a two-minute non-event. This is also why feature flags are valuable — they let you "roll back" a feature by flipping a flag, faster than even a deploy rollback. A deployment system's rollback capability matters more than its deploy speed.
5. Why are flaky tests so damaging to a CI/CD pipeline? Because they destroy the trust that makes the pipeline useful. A flaky test — one that passes and fails non-deterministically on the same code — trains developers to respond to a failure by re-running the pipeline rather than investigating, since "it's probably just flaky." Once that habit forms, a *real* failure — an actual bug the test correctly caught — gets the same treatment: re-run, dismiss, ship anyway. So the tests stop being a reliable gate; the pipeline reports failures that everyone ignores, which is worse than no tests because it provides false confidence. It's exactly the dynamic of alert fatigue: too many false signals and people tune out all signals, including the true ones. The damage compounds — flaky tests also slow the pipeline (re-runs), cause spurious deploy blocks, and erode belief in the whole automated-testing investment. The fix is aggressive: fix flaky tests immediately, or quarantine them (remove from the blocking gate) until fixed, so that a red pipeline always means a genuine problem worth stopping for.

Further reading