← System design

Infrastructure

Design Code Deployment Pipeline

Every outage you have read about has a deploy somewhere near the start of the timeline. A deployment pipeline exists to make shipping boring: the same commit always produces the same artifact, and a bad artifact meets a few hundred users before it meets everyone. At 3,000 deploys a day, "boring" has to be automatic, because no human can watch them all.
Core: content-addressed artifacts + judged canaries3,000 production deploys a dayCorrectness over speedBad change caught at under 2% of traffic

01

What the pipeline must do

02

What the pipeline must do

Functional
  • On every merge to main, build each affected service, run its tests and publish an immutable artifact.
  • Engineers deploy an artifact to staging or production by naming its digest, or let a service deploy automatically after merge.
  • Production rollouts go in waves: a canary slice, then a region, then the rest, with health checks between each.
  • A rollout pauses or rolls back automatically when error rate, latency or saturation gets worse than the baseline.
  • Anyone can see what version runs where, who shipped it, and what changed since the last one.
  • Teams can freeze deploys for a service or the whole company, with an audited override.
Non-Functional
  • Builds are reproducible: the same source and toolchain give a byte-identical artifact.
  • Only artifacts built by the trusted pipeline, with signed provenance, can run in production.
  • Median merge to production under 30 minutes for a typical service.
  • Rollback to the previous version completes within 3 minutes for any service.
  • The pipeline itself never becomes the reason nobody can ship a fix during an incident.
Clarify the starting point

Ask whether the company runs a monorepo or many repos, whether services run on Kubernetes, VMs or both, and whether database migrations ship with code. Those three answers decide how you detect what to build, what a deploy actually does on a host, and whether rollback is always safe. Also ask who the users are: a pipeline for 40 engineers and one for 4,000 are different products.

02

Sizing the pipeline

03

Sizing the pipeline

Assume 4,000 engineers working on 800 services across six regions. About 6,000 changes merge each day, and each pull request is also built several times before it merges. Production runs about 120,000 containers.

QuantityWorkingResult
Builds per day6,000 merges + about 19,000 pull request buildsabout 25,000
Build minutes per day25,000 x 12 min average300,000 minutes
Average concurrent builds300,000 / 1,440about 210
Peak concurrent builds3x average in the working-hours overlapabout 630 workers of 8 vCPU
Production deploys per dayassumption, many services auto-deployabout 3,000
New artifact data per buildcontainer image of about 400 MB, about 60 MB in new layersabout 1.5 TB a day
Artifact storage30 days of every build plus every released version foreverabout 60 TB and growing slowly
Bytes pulled for deploys3,000 deploys x 150 hosts x 60 MB of new layersabout 27 TB a day
Canary metrics3,000 deploys x 20 metrics x 2 groups, sampled every 10 slight, but every query must be fast
Where the load really is

Build compute dominates cost and artifact pulls dominate network. Both are cut sharply by caching: a remote build cache skips work whose inputs have not changed, and regional registry mirrors keep 27 TB a day of pulls off the central store.

03

API design

04

API design

POST/v1/buildsStart a build for a commit
{ "repo": "monorepo", "commit": "3e9a1c7", "targets": ["//services/checkout:image"], "reason": "merge" } // 202 { "build_id": "b_88213", "status": "queued" }
GET/v1/builds/{build_id}Build status and outputs
{ "build_id": "b_88213", "status": "succeeded", "cache_hit_ratio": 0.83, "artifacts": [ { "target": "//services/checkout:image", "digest": "sha256:4b7f...e1", "provenance": "att_5521" } ] }
POST/v1/rolloutsRoll an artifact out to an environment
{ "service": "checkout", "environment": "production", "digest": "sha256:4b7f...e1", "strategy": "default-waves", // canary 1%, one region, then remaining regions "change_ticket": "CHG-20411" } // 202 { "rollout_id": "ro_3302", "state": "canary" } 409 if a freeze or another rollout is active
POST/v1/rollouts/{rollout_id}/rollbackStop and return to the last good version
{ "reason": "p99 latency up 40% in canary" } // Rolls every wave back to the previous digest, regardless of how far the rollout got
GET/v1/services/{service}/versionsWhat runs where
{ "production": { "us-east": "sha256:4b7f...e1", "eu-west": "sha256:a02c...9d" // still on previous, wave 3 pending } }

04

Architecture overview

05

Architecture overview

From merge to productionDiagram
flowchart LR
	G["Git: merge to main"] --> CI["CI orchestrator"]
	CI --> Q[["Build queue"]]
	Q --> W["Ephemeral build workers"]
	W --> RC[("Remote build cache")]
	W --> AR[("Artifact registry, by digest")]
	W --> PV["Provenance signer"]
	PV --> AR
	AR --> MR[("Regional mirrors")]
	CI --> DS["Deploy service: desired versions"]
	DS --> RO["Rollout controller"]
	RO --> CL["Clusters in 6 regions"]
	CL --> MR
	CL --> MT[("Metrics store")]
	MT --> CJ["Canary judge"]
	CJ --> RO

The pipeline has two halves joined by one thing: an artifact digest. The build half turns a commit into a signed artifact whose name is the SHA-256 of its contents. The deploy half takes a digest and moves it through environments. Nothing in the deploy half ever rebuilds, so what was tested is exactly what ships.

Build workers are ephemeral. Each job gets a fresh sandbox with no network access except to the cache and a pinned dependency mirror, runs the build and is destroyed. Before running any step, the worker hashes that step's inputs and asks the remote cache whether the output already exists. In a large monorepo, 80% or more of steps are cache hits, which is what keeps a 12-minute average from becoming an hour.

The deploy service stores desired state: for each service and environment, which digest should run and with what rollout strategy. The rollout controller compares desired with actual, moves traffic in waves and asks the canary judge after each wave whether to continue. Admission control in every cluster refuses any image without a valid signature from the pipeline's provenance signer.

CI orchestrator

Finds the targets affected by a commit from the build graph, schedules them and records results. Prioritises merge builds over pull request builds when the queue is long.

Artifact registry

Content addressed and immutable. Tags are human labels pointing at digests; deploys always use the digest, never a tag such as latest.

Rollout controller

A reconcile loop per rollout: shift a slice of traffic, wait, ask the judge, continue or reverse. Survives its own restart because state lives in the deploy service.

Canary judge

Compares canary pods with baseline pods started at the same time on the old version, metric by metric, and returns pass, fail or inconclusive.

05

Deep dive: builds you trust, rollouts that judge themselves

06

Deep dive: builds you trust, rollouts that judge themselves

Three things that make deploys safe

Builds you can reproduce, artifacts you can trust, and rollouts that notice trouble before a human does. Each is a separate mechanism.

Hermetic, cacheable builds

A build is hermetic when its output depends only on declared inputs: source files, pinned dependency versions by hash, and a pinned toolchain image. No reading the clock, the network or the host's installed packages. Tools such as Bazel or Buck enforce this by running each action in a sandbox that only sees its declared inputs.

Hermeticity is what makes caching safe. The cache key for an action is the hash of its command line and every input file. If two engineers, or two CI runs, hit the same key, the second just downloads the output. Without hermeticity, a cache hit might hide a difference in environment, and you end up with an artifact that works only because of what happened to be on one worker. That is the classic "works in CI, breaks in prod" story.

Trusting the artifact

When a build finishes, the signer writes a provenance statement: which commit, which build definition, which builder, which input digests. It signs that statement with a key only the pipeline holds and stores it next to the artifact. This follows the shape of the SLSA framework. Clusters run an admission check that verifies the signature and refuses anything that did not come from the pipeline, so a laptop-built image or a tampered registry entry cannot run in production.

Pin third-party dependencies by hash in a lockfile and pull them through an internal mirror. When a public package is compromised, you can answer "which of our artifacts contain it" from provenance data in minutes instead of searching every repo.

Waves and the canary judge

A rollout with automatic analysisDiagram
flowchart TD
	ST["Rollout starts with digest D"] --> C1["Wave 1: 1% in one region, plus matching baseline"]
	C1 --> J1{"Judge after 10 minutes"}
	J1 -->|fail| RB["Roll back every wave"]
	J1 -->|inconclusive| EX["Extend wave, collect more data"]
	EX --> J1
	J1 -->|pass| C2["Wave 2: 100% of first region"]
	C2 --> J2{"Judge after 15 minutes"}
	J2 -->|fail| RB
	J2 -->|pass| C3["Wave 3: remaining regions, two at a time"]
	C3 --> J3{"Final judge and bake time"}
	J3 -->|pass| DONE["Mark D as last good"]
	J3 -->|fail| RB

Comparing a canary against the whole fleet is misleading, because new pods have cold caches and fresh JIT state. Start a small baseline group on the old version at the same moment as the canary, and route equal traffic to both. Now the only difference between the groups is the code.

For each metric, such as error rate, p50 and p99 latency, CPU per request and memory growth, the judge runs a statistical test, for example Mann-Whitney on latency samples, and classifies the difference as better, same or worse beyond a threshold. A rollout fails if any critical metric is worse, and is inconclusive when there is too little traffic to tell. Low-traffic services get longer waves rather than weaker tests.

Bake time matters for slow failures. Memory leaks, cache poisoning and daily batch jobs do not show up in ten minutes. The final wave waits for a bake period before the digest is marked last good, the target every future rollback uses.

Coordinating 3,000 rollouts a day

Each rollout is a record in the deploy service with a state: pending, canary, wave_n, baking, done, rolling_back or rolled_back. Only one active rollout per service and environment is allowed, enforced by a conditional write on that pair. A new digest arriving while one is in flight either queues behind it or, if the author asks, supersedes it, in which case the controller finishes the current wave and switches target.

Rollouts also interact. If a shared dependency such as the auth service is mid-rollout and a downstream service starts failing, the judge may blame the wrong change. Record every active rollout per region with start times, and when a judge fails a wave, show the other rollouts that overlapped it. Company-wide freezes are just a flag the deploy service checks before admitting any new rollout, with an override that needs a second approver and is logged.

Every state change is an event. The what-runs-where page, Slack notifications and the audit log are all projections of that stream, so they never disagree with the controller.

Separating deploy from release

Feature flags let code reach production switched off and then be enabled for 1%, 10% and 100% of users independently of the deploy. That shrinks what a deploy can break: a bad deploy is mostly a crash or performance problem, which canaries catch well, while a bad feature is turned off with a flag in seconds. Flags need hygiene, an owner and an expiry date each, or the codebase fills with dead branches.

Rollback is only safe if the schema allows it

If version N+1 runs a migration that drops a column version N reads, rolling back breaks everything. Use expand and contract: add the new column first, ship code that writes both, backfill, move reads, and only drop the old column in a later release once no running version needs it.

06

Designs that look fine and are not

08

Designs that look fine and are not

🏷️
Deploying by mutable tag

A deploy references checkout:latest. Between staging and production someone pushes a new image under the same tag, and production gets code that was never tested.

Always deploy by digest. Tags are for humans to read; the deploy record stores the digest.
🔨
Rebuilding for each environment

Staging and production each run their own build from the same commit. A changed base image or dependency between the two builds produces different binaries.

Build once, promote the same artifact through every environment, and inject environment differences as configuration.
🌍
Big-bang deploys to every region at once

A bad change reaches all users within a minute and there is no healthy region to compare with or fail over to.

Roll out in waves with a small canary first and region boundaries as natural blast-radius limits.
👀
Humans watching dashboards as the safety check

At 3,000 deploys a day nobody can watch them all, and a tired engineer misses a 30% latency rise at 2 a.m.

Automate canary analysis with a baseline, and make rollback the default action when the judge fails a wave.
🧪
Shared, long-lived build machines

State leaks between builds, a compromised job can poison later ones, and "clean the workspace" becomes a ritual.

Run every build in a fresh sandbox and destroy it afterwards; rely on the remote cache, not local leftovers, for speed.

07

Decisions and their costs

09

Decisions and their costs

Chosen
Hermetic builds with a remote cache

Reproducible outputs, safe caching and big speed-ups in a monorepo. Costs real migration effort: every build rule must declare its inputs, and some tools fight sandboxing.

Pays back at this size
Alternative
Conventional scripts in containers

Fast to set up and familiar. Caching is coarse and occasionally wrong, and reproducibility depends on discipline. Fine for a few dozen services.

Good for small teams
Chosen
Canary with a live baseline and statistical judge

Catches regressions at about 1% of traffic, with few false alarms because canary and baseline share conditions. Needs good metrics per pod and enough traffic.

Automatic and trustworthy
Alternative
Blue-green switch

Two full environments and one traffic flip. Rollback is instant, but the cost is double capacity during deploys and every user moves at once.

Useful for stateful or tiny services
Chosen
Push-based rollout controller

The controller drives each wave and can react within seconds. It is a central component that must be highly available.

Fast reaction
Alternative
GitOps pull model

Clusters reconcile from a Git repo of desired state, giving a clean audit trail and simple disaster recovery. Wave orchestration and fast rollback need extra tooling on top.

Strong for config and audit

08

Failure modes

10

Failure modes

⏳
Build queue backs up for 40 minutes

A change to a shared library invalidates the cache for 600 services at once just as everyone merges before a holiday.

Prioritise merge builds and hotfixes over pull request builds, autoscale workers from queue depth, and warn authors before merging changes with a huge rebuild footprint.
🤷
Canary passes a bad build

The regression only affects one rare request type, which the canary saw too few times to judge.

Weight metrics by endpoint, extend waves for low-traffic services, add synthetic probes for critical paths and keep bake time before marking last good. Track escaped regressions and tune thresholds from them.
🧱
Registry returns a corrupted layer

A storage fault serves a layer whose bytes do not match its digest. Hosts fail to start the new version.

Clients verify digests on pull and retry from another mirror. Because artifacts are content addressed, corruption is detectable by construction and never runs silently.
↩️
Rollback breaks things further

Version N+1 wrote data in a new format that version N cannot read. Rolling back turns a latency problem into errors.

Require backward-compatible data and schema changes, test rollback as part of the canary wave, and block rollouts whose migrations are not reversible without an explicit override.
🦠
A compromised dependency

A popular open source package ships a malicious release, and it is pulled into builds within hours.

Pin dependencies by hash, pull only through a mirror that delays brand-new versions, scan in the pipeline, and use provenance to list affected artifacts and block their deploy at admission.
🚧
The pipeline is down during an incident

The CI orchestrator's database fails while production is on fire, and nobody can ship the fix.

Run the deploy path independently of CI, keep a break-glass procedure that deploys a known signed digest directly, and drill it regularly.

09

How to handle the interview

11

How to handle the interview

1

Split build from deploy. Say "build once, deploy the same digest everywhere" early. It is the backbone of the design.

2

Explain why hermetic matters. Tie it to caching and reproducibility. Without declared inputs, a cache hit can be a lie.

3

Describe the canary with a baseline. Most candidates compare canaries with the whole fleet. A same-age baseline and a statistical test stand out.

4

Make rollback automatic. At thousands of deploys a day, a failed judgement should reverse the rollout without paging anyone.

5

Mention schema compatibility. Expand and contract migrations show you know rollback has limits.

6

Cover supply chain security. Signed provenance and admission control are now standard expectations, not extras.

7

Plan for the pipeline failing. A break-glass path proves you thought about the worst day, not the average one.

10

Growing the pipeline in stages

13

Growing the pipeline in stages

Earn each stage

Every step below removes a pain you can point to. Do not build a canary judge before you have per-pod metrics worth judging.

Stage 1: one team, a few services
Hosted CI and a deploy script

Build a container on merge, push it to a registry, run a rolling update. Rollback is redeploying the previous tag.

Stage 2: dozens of services
Immutable digests and staged environments

Deploy by digest, promote the same artifact from staging to production, and add a deploy service that records what runs where.

Stage 3: hundreds of services
Waves, canaries and automatic rollback

Rollouts move in waves with a baseline comparison, and failed judgements reverse automatically. The human dashboard check is no longer the safety net.

Stage 4: thousands of engineers
Hermetic builds, remote cache, signed provenance

Build graphs decide what to rebuild, caches cut build time, and admission control enforces that only pipeline-built artifacts run.

Stage 5: continuous everything
Per-commit auto-deploy with flags

Most services deploy every merge on their own, releases are controlled with flags, and humans step in only when the judge says inconclusive.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.