Design Continuous Deployment System: System Design & Architecture

Build the system that takes CI's validated artifact the last mile into production: promote it through environments, shift real user traffic onto it a little at a time, watch health signals for evidence it is safe, and reverse everything in seconds the moment it is not.

Scope

In Scope

  • The CI handoff: consuming immutable, validated artifacts - the explicit boundary where Design a CI System ends and this design begins
  • Environment promotion: dev → staging → production, with policy gates, manual approvals, and deployment windows
  • Progressive rollout with canary as the default strategy (rolling and blue-green as deliberate alternatives)
  • Health-gated automation: SLO evaluation during rollout, and automated rollback on breach
  • Feature flags as the second safety layer: decoupling deploying code from releasing behavior
  • Deployment history and audit: who deployed what, where, when, and what the health evidence said

Out of Scope

  • Building and testing the artifact: that is Design a CI System - this system never compiles anything, it only promotes what CI produced
  • The metrics/monitoring platform itself: we consume its queries as the health signal; building it is an observability design of its own
  • General infrastructure provisioning (Terraform-style IaC): we deploy onto environments that exist; per-environment app config is in scope, cloud plumbing is not
  • Incident response and paging: we emit the events; the on-call machinery is separate
  • Compliance suites (SOX/change-management workflows): we keep the immutable audit log and approval hooks they consume

💡 Where CI ends and CD begins: the artifact is the contract

The boundary between the two systems is a single object: the artifact - a container image or binary that CI built, tested, checksummed, and versioned. CI's final statement is "these exact bytes passed everything." This system's first rule is to never break that statement: we deploy those bytes, unmodified, to every environment. That rule has a name - build once, deploy many - and violating it is a classic production bug: if each environment rebuilds from source, then staging tested one build and production received a different one, and the promotion pipeline was theater. Anything that must differ between environments (database URLs, API keys, tuning) is injected as configuration at deploy time, never baked into the artifact. It also sets this system's character. CI answers "is this code good?" with tests in a lab. CD answers a question no lab can: "does this version survive real production traffic?" The only way to answer it is to run the experiment on production - and so this entire design is about running that experiment with the smallest possible blast radius and an instant undo.

Functional Requirements

  • When CI publishes a validated artifact, the system can promote it through environments according to each service's pipeline policy
  • A deployment runs a chosen strategy - canary by default: traffic shifts to the new version in steps, gated on health
  • Health degradation during rollout triggers automated rollback; a manual rollback button is always available and never requires approval
  • Production deploys can require manual approval and honor deployment windows
  • The same artifact receives per-environment configuration at deploy time (build once, deploy many)
  • Deployments of different services run concurrently; deployments of the same service to the same environment are serialized
  • Every deployment leaves a complete record: initiator, artifact, approvals,每 health verdict, timeline, outcome

Non-functional Requirements

  • Rollback speed: automated detection-to-restored in under ~2 minutes; the manual button restores traffic in seconds
  • Deploy start: < 60 seconds from trigger (or approval) to first instances updating
  • Zero-downtime rollouts: the error budget spent by a healthy deployment rounds to zero
  • Scale: ~10K services, ~20K deployments/day - and rollbacks are routine (a few percent of deploys), so rolling back must be boring, not heroic
  • Audit: the deployment record is append-only and immutable
  • The control plane must outlive what it deploys: deployments are how incidents get FIXED, so this system must work precisely when everything else is broken

💡 What is an SLO, in plain words - and how does it become an automated decision?

An SLI (service level indicator) is a measured signal about how a service is behaving: the fraction of requests that fail, the p99 latency, or something business-shaped like checkout completion rate. An SLO (service level objective) is the promise drawn on top of it: "99.9% of requests succeed," "p99 stays under 400ms." In most systems SLOs are dashboards for humans. In this system they become executable: the canary's health gate is an SLO comparison evaluated by software every 30 seconds, and the software - not a person - decides whether the rollout proceeds or reverses. The crucial subtlety is WHAT to compare against. An absolute threshold ("error rate must stay under 1%") breaks in practice, because error rates move with time of day, traffic mix, and upstream weather. The honest comparison is canary versus baseline: the new version and the old version are serving the same traffic at the same minute, so any difference between their SLIs is attributable to the version - the deployment is a controlled experiment, with the old version as the control group. "Canary error rate may not exceed baseline's by more than 0.5 points" survives contact with reality; "under 1% always" does not. This is why the design insists the baseline keeps running and serving during the whole rollout: it is not just the rollback target - it is the control group that makes the health verdicts meaningful.

Back-of-the-Envelope Estimates

  • Assume ~10K services, ~20K deployments/day ≈ 0.23/sec average; deploys cluster in business hours and release windows → ~1/sec sustained, more during Monday release trains
  • A typical rollout: canary steps of 5% → 25% → 50% → 100%, baking ~10 minutes per step → ~40 minutes end to end
  • Concurrency via Little's Law (as in Design a CI System): ~1 deploy/sec x ~2,400 seconds ≈ ~2,400 rollouts in flight at busy times - a tiny control-plane load
  • Health evaluation is the biggest number, and it is still small: ~2,400 active rollouts x ~10 SLI queries every 30 seconds ≈ ~800 queries/sec against the metrics platform - our heaviest external dependency
  • Rollbacks: at a realistic 2-5% of deploys, that is 400-1,000 rollbacks/day. Read that again: rollback happens hundreds of times a day. It must be a boring, single-action, well-oiled path - not an emergency procedure
  • Storage: deployment records, verdicts, and audit events at a few KB each ≈ nothing; PostgreSQL yawns
  • The punchline: this is the lowest-QPS system in this practice set and the highest-stakes per operation. Its scale axis is not throughput - it is blast radius

💡 What the estimates mean: the scale axis is blast radius, not throughput

Every other design in this set scales along some volume axis - requests, messages, bytes, CPU-seconds. This one does not: a few thousand concurrent rollouts is nothing, and PostgreSQL plus a small worker fleet runs the whole control plane. What grows instead is consequence: each deployment is a change to a live system with real users, and the design's job is to control how much damage a bad version can do before it is detected and reversed. Read every mechanism in this design through that lens and the system becomes coherent. Canary percentages keep the experiment small (5% of users meet the new version before 100% do). Bake times and SLO gates keep it measured (exposure grows only after evidence). The always-running baseline keeps it reversible (rollback is a traffic shift, not a scramble). Feature flags keep behavior decoupled from binaries (a bad feature dies by toggle, not by redeploy). Approval gates and windows keep humans in the loop exactly where judgment beats automation. Small, measured, reversible, decoupled - four properties, one goal. Two structural notes fall out. First, the one heavy integration is the metrics platform (~800 queries/sec) - this system is a decision-maker wrapped around someone else's measurements, and its verdict quality is capped by the quality of those signals. Second, the reliability inversion from the requirements: because deployments are how incidents get fixed, the deploy system must not share fate with what it deploys - it runs on its own isolated infrastructure, and its failure modes must always leave traffic in a safe, static state. The doctor cannot be one of the patients.

High-level Architecture

  • Artifact Registry: receives CI's output - immutable, checksummed, versioned artifacts with provenance (which CI build produced them); tags never move
  • Deployment Orchestrator: a state machine per rollout (created → approved → step N baking → promoting → succeeded / rolled_back), run by stateless workers over PostgreSQL state - the same control-plane shape as Design a CI System's scheduler
  • Strategy Engine: executes the chosen rollout shape - canary steps by default; rolling or blue-green where a service's policy says so
  • Traffic Controller: the hands - shifts traffic weights between versions via the load balancer / service mesh (5% → 25% → ...), and shifts them back in seconds on rollback
  • Health Evaluator: the eyes - queries the metrics platform every 30s for canary AND baseline SLIs, compares them, and records a verdict that gates the next step
  • Approval & Policy Gate: manual approvals for production, deployment windows, and the rule that rollback never needs approval
  • Config & Secrets Injection: per-environment configuration applied at deploy time to the unmodified artifact (build once, deploy many)
  • Audit Log: append-only record of every action and every health verdict - the flight recorder

Data/User Flow Diagram

[CI (Design a CI System)] --validated artifact v2.4.1--> [Artifact Registry]
                                                    (immutable, checksummed,
                                                     provenance recorded)
                                                              |
                                    promotion policy: dev auto -> staging auto
                                    -> prod needs approval + window
                                                              v
[Approval Gate] --approved--> [Deployment Orchestrator (state machine)]
                                                              |
                                                              v
                                              [Strategy Engine: canary]
                                                              |
                        step 1: canary instances get v2.4.1   v
[Traffic Controller] ---- 5% ----> [canary: v2.4.1]
                     ---- 95% ---> [baseline: v2.3.8]  (keeps running:
                                                        control group AND
                                                        rollback target)
                                                              |
                              every 30s during the bake       v
[Health Evaluator] <---- SLI queries ---- [Metrics Platform (external)]
     compare canary vs baseline: error rate, p99, custom SLIs
             |                                    |
      healthy for 10 min                   breach, 2 windows in a row
             v                                    v
     next step: 25% -> 50% -> 100%          [Rollback Engine]
     then drain baseline                     shift 100% -> baseline (seconds)
             |                               mark failed + store evidence
             v                                    |
        [Audit Log: who, what, where, every verdict, full timeline]

💡 Canary vs blue-green vs rolling: pick a default and defend it

Three standard strategies, one honest comparison - and a house default, because "it depends" with no default is a non-answer. Rolling update: replace instances a few at a time until done. Cheapest (no extra capacity), but there is no traffic control and no measurement gate - by the time you notice a problem, most of the fleet may be running it, and "rollback" means deploying the old version again, which takes as long as deploying did. Right for dev and staging, where the blast radius is developers. Blue-green: run a complete duplicate environment (green) with the new version, test it, then switch ALL traffic at once; rollback is switching back - instant and total. The costs: 100% duplicate capacity, and exposure is all-or-nothing - the switch is instant, but so is the blast radius, and no gradual evidence was gathered on the way. Canary: run the new version on a small slice, shift traffic in measured steps, gate each step on health versus baseline. Costs ~5% extra capacity instead of 100%, gathers real-traffic evidence BEFORE committing everyone, and rollback stays a seconds-fast traffic shift the whole way (the baseline never stopped running). The price is time (~40 minutes, not one switch) and a real dependency on good metrics. The defense of canary as default: it is the only strategy that measures before it commits, and this whole system's thesis is blast-radius control. Blue-green earns its double cost where instant, total cutover matters more than gradual evidence (a payment-provider migration you cannot run half-and-half). And one warning that catches people: blue-green does NOT dodge the database problem - both colors share one database, so the schema-compatibility discipline (see the migrations flow) applies to every strategy equally.

Data Model & Storage

  • Artifacts (PostgreSQL + registry storage): version, checksum, provenance (repo, commit, CI build id), created_at. Immutable rows for immutable bytes - a tag, once written, never moves.
  • Services & Environments (PostgreSQL): each service's environments, the version currently serving in each, instance targets, and the service's pipeline policy (strategy, steps, gates, windows).
  • Deployments (PostgreSQL): the state-machine rows - service, environment, artifact, strategy config (the canary steps and thresholds), current step, status, timestamps per transition. One rollout, one row, serialized per (service, environment).
  • Health Verdicts (PostgreSQL, append-only): every 30-second evaluation stored - the SLI values for canary and baseline, the comparison result, and the decision it fed. The evidence, not just the outcome.
  • Approvals (PostgreSQL): who approved what, when, with what conditions - and, structurally, no approval rows exist for rollbacks, because rollback never waits for one.
  • Audit Events (PostgreSQL, append-only): every action by human or automation, in order. Immutable because an editable audit log is not an audit log.
  • Config (PostgreSQL, versioned per service + environment): the key-values injected at deploy time; secrets stay in the secrets manager and are referenced, never stored here.

💡 Why every 30-second health verdict is stored forever

It would be cheaper to keep only outcomes - "deployed" or "rolled back." Storing every intermediate verdict is a deliberate choice with three payoffs. First, the 2am question. When software rolls back a deployment automatically, the on-call engineer's first question is "why?" - and the answer must be evidence, not a shrug: "at 14:32, canary error rate was 2.1% against a baseline of 0.3%, breaching the 0.5-point margin for two consecutive windows." Automated decisions that affect production must be able to show their work, or engineers learn to distrust and disable the automation - and a disabled safety system is worse than none, because everyone believes it is still there. Second, tuning. Thresholds and bake times are guesses until history grades them: verdicts from hundreds of rollouts reveal which gates flap on noise (too tight), which breaches were caught at 5% that would have been outages at 100% (the system paying for itself), and which slipped through (bake too short - lengthen it). Third, the audit story. The deployment record - who triggered it, who approved it, every verdict, every transition, the outcome - is the flight recorder for the riskiest routine operation in software. Append-only is non-negotiable for the same reason flight recorders do not have erase buttons. The storage cost of all this honesty, per the estimates: approximately nothing. There is no tradeoff here to agonize over - just a decision to keep the evidence.

API Design

POST /v1/deployments
Start a deployment: service, environment, artifact version, strategy (or the service's default policy).
GET /v1/deployments/:id
Live rollout state: current step, traffic split, latest health verdicts, timeline.
POST /v1/deployments/:id/approve
Satisfy a manual gate (prod). Approver identity recorded; approver may not be the initiator where policy says so.
POST /v1/deployments/:id/rollback
The big red button: shift all traffic to baseline now. Never requires approval.
POST /v1/deployments/:id/pause
Freeze the rollout at its current (safe, static) traffic split; resume to continue.
GET /v1/services/:id/environments
What version is serving where, right now.
GET /v1/deployments?service=...&cursor=...
History: the append-only record, paginated.

Low-level Design

The handoff and promotion - build once, deploy many (step by step):

  • 1) CI publishes the artifact; the registry verifies the checksum and records provenance - version v2.4.1 is forever those exact bytes, traceable to the commit and CI build that produced them.
  • 2) Policy takes over: dev deploys automatically on every artifact; staging on merge to main; production waits for a human approval inside a deployment window.
  • 3) At each environment, the SAME artifact is deployed - what differs is injected configuration (database URLs, feature-flag endpoints, tuning), applied at deploy time.
  • 4) The anti-pattern this kills: per-environment rebuilds, where staging validated different bytes than production received and the whole promotion pipeline was theater.
  • 5) Approvals record who and when; and note the deliberate asymmetry - promoting forward can require approval, rolling back never does. Safety must not wait in a queue.

Canary rollout - the controlled experiment (the genuinely tricky part, step by step):

  • 1) The orchestrator creates the rollout state machine from the service's policy: steps [5, 25, 50, 100], bake 10 minutes each, thresholds defined against baseline.
  • 2) Canary instances running v2.4.1 come up ALONGSIDE the baseline fleet - the old version keeps serving; it is both the control group and the instant rollback target, and that dual role is the whole trick.
  • 3) The traffic controller shifts 5% of traffic to the canary.
  • 4) The bake: every 30 seconds the health evaluator queries the metrics platform for canary and baseline SLIs separately (metrics are labeled by version) and records a verdict: error rate within margin of baseline? p99 within ratio? enough requests to judge at all?
  • 5) Ten minutes of healthy verdicts → promote to 25%, and the cycle repeats. Exposure only ever grows AFTER evidence - that is the sentence that defines canary.
  • 6) At 100%, the baseline drains but stays warm briefly (cheap insurance for a late-breaking issue), then the rollout closes as succeeded.
  • 7) A statistical honesty rule throughout: 5% of a low-traffic service may be a handful of requests - too few to judge. The min-sample guard extends the bake rather than promoting on noise; no data is never treated as good news.

Automated rollback - boring by design (step by step):

  • 1) A breach is two consecutive bad windows, not one - a single 30-second blip rolls nothing back (flap protection); a sustained signal does.
  • 2) On breach: freeze the rollout, and shift ALL traffic back to baseline. This takes seconds, because the baseline never stopped running - rollback is a traffic-weight change, not a deployment.
  • 3) The deployment is marked rolled_back with its verdict evidence attached; the on-call notification carries the numbers, not just the fact.
  • 4) End-to-end: breach detected within ~60-90 seconds of onset, traffic restored seconds later - inside the 2-minute promise.
  • 5) The manual big red button takes the identical path, is available at every moment of every rollout, and never asks for approval.
  • 6) Per the estimates, this path runs hundreds of times a day across the fleet. That frequency is a feature: a rollback that is routine gets invoked early and casually; a rollback that is an emergency procedure gets invoked late, after someone spent twenty minutes hoping the graph would improve.

Feature flags - the second lever (step by step):

  • Deploy and release are different events: deploy = new bytes are running; release = users experience new behavior. Flags decouple them.
  • 1) Risky features ship dark - the code deploys wrapped in a flag that is off, so the deployment itself changes nothing user-visible.
  • 2) Release becomes its own gradual, targeted act: enable for employees, then 1% of users, then everyone - orthogonal to which binary is running.
  • 3) The kill switch: a misbehaving feature dies by toggle in seconds, with no pipeline, no redeploy, no traffic shift.
  • Two levers, two failure classes: rollback answers "this BINARY is bad" (crashes, latency, resource leaks); flag-off answers "this FEATURE is bad" (broken flow, bad business outcome) - which canary may never catch, especially after 100%.
  • The honesty note: flags accumulate into debt - dead flags make code unreadable and create untested combinations - so shipped flags get an owner and a removal date, or the safety mechanism slowly becomes the hazard.

Database migrations - the part rollback cannot save (step by step):

  • The uncomfortable truth: traffic rolls back in seconds; schemas do not roll back at all. If the deploy that shipped v2.4.1 also dropped a column v2.3.8 reads, then rolling back the traffic restores a version the database can no longer serve - the safety net has a hole exactly where the fall happens.
  • Worse, canary makes the constraint structural: during every rollout, old and new versions run SIMULTANEOUSLY against ONE database - so N and N-1 schema compatibility is not best practice, it is a precondition of the strategy.
  • The discipline is expand-migrate-contract: 1) EXPAND - add the new column/table; old code ignores it; deploy. 2) MIGRATE - new code writes both shapes and reads the new one; backfill runs; bake with confidence. 3) CONTRACT - only in a LATER deploy, after the old version will never run again, remove the old column.
  • The rule in one line: never couple a destructive migration to the deploy that needs it - destructive changes ride alone, after the fleet has moved on.
  • Every strategy shares this constraint, including blue-green: two colors, one database.

Database Types and Schemas

Database Schemas (PostgreSQL - the control plane is deliberately small)

- artifacts
  artifact_id UUID PRIMARY KEY,
  service_id UUID NOT NULL,
  version VARCHAR(100) NOT NULL,       -- "v2.4.1"
  checksum CHAR(71) NOT NULL,          -- "sha256:..." verified on receipt
  provenance JSONB NOT NULL,           -- { repo, commit_sha, ci_build_id }
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (service_id, version)         -- tags never move: immutable bytes

- deployments  (one rollout = one state-machine row)
  deployment_id UUID PRIMARY KEY,
  service_id UUID NOT NULL,
  environment env_enum NOT NULL,       -- dev | staging | production
  artifact_id UUID NOT NULL,
  strategy strategy_enum DEFAULT 'canary',
  strategy_config JSONB,
    -- { steps: [5,25,50,100], bake_minutes: 10,
    --   gates: { error_rate_margin: 0.005, latency_p99_ratio: 1.2,
    --            min_requests_per_window: 500 } }
  status deploy_status_enum,           -- pending_approval | rolling_out |
                                       -- paused | succeeded | rolled_back
  current_step INT DEFAULT 0,
  initiated_by UUID NOT NULL,
  created_at TIMESTAMP, started_at TIMESTAMP, finished_at TIMESTAMP,
  -- one rollout at a time per service+environment:
  UNIQUE (service_id, environment)
    WHERE status IN ('pending_approval','rolling_out','paused')

- health_verdicts  (append-only: the evidence)
  verdict_id BIGSERIAL PRIMARY KEY,
  deployment_id UUID NOT NULL,
  step INT, evaluated_at TIMESTAMP DEFAULT NOW(),
  canary_slis JSONB,      -- { error_rate: 0.021, p99_ms: 512, requests: 8140 }
  baseline_slis JSONB,    -- { error_rate: 0.003, p99_ms: 240, requests: 154300 }
  result verdict_enum,    -- healthy | breach | insufficient_data
  detail TEXT             -- "error_rate 2.1% exceeds baseline 0.3% + 0.5pt margin"

- approvals
  approval_id UUID PRIMARY KEY,
  deployment_id UUID NOT NULL,
  approver_id UUID NOT NULL,           -- policy may require != initiated_by
  approved_at TIMESTAMP DEFAULT NOW(),
  conditions JSONB
  -- structurally absent: approvals for rollbacks. Rollback never waits.

- audit_events (append-only; an editable audit log is not an audit log)
  event_id BIGSERIAL PRIMARY KEY,
  deployment_id UUID, actor VARCHAR(100),   -- human id or "health-evaluator"
  action VARCHAR(100),                       -- approved | step_promoted |
                                             -- rollback_triggered | ...
  detail JSONB, at TIMESTAMP DEFAULT NOW()

- configs (versioned per service + environment)
  service_id UUID, environment env_enum, version INT,
  values JSONB,                -- injected at deploy time
  secret_refs JSONB,           -- pointers into the secrets manager, never values
  PRIMARY KEY (service_id, environment, version)

API Request/Response Payloads

POST /v1/deployments
Request
{
  "service_id": "svc_payment",
  "environment": "production",
  "artifact_version": "v2.4.1",
  "strategy_config": {
    "steps": [5, 25, 50, 100],
    "bake_minutes": 10,
    "gates": {
      "error_rate_margin": 0.005,
      "latency_p99_ratio": 1.2,
      "min_requests_per_window": 500,
      "custom_slis": ["checkout_completion_rate"]
    }
  }
}
Response
{
  "deployment_id": "dep_8f3e2d1c",
  "status": "pending_approval",
  "approval_url": "https://cd.example.com/approvals/dep_8f3e2d1c",
  "window": "weekdays 09:00-16:00 UTC",
  "estimated_duration_minutes": 45
}

// Production policy requires a human approval inside the window. The
// gates compare canary AGAINST BASELINE (margins and ratios), not
// against absolute numbers - the old version serving the same traffic
// is the control group.
GET /v1/deployments/:id (mid-rollout)
Response
{
  "deployment_id": "dep_8f3e2d1c",
  "service": "payment-service",
  "environment": "production",
  "status": "rolling_out",
  "traffic": {
    "canary_version": "v2.4.1", "canary_percent": 25,
    "baseline_version": "v2.3.8", "baseline_percent": 75
  },
  "current_step": { "step": 2, "of": 4, "baking_minutes_left": 4 },
  "latest_verdict": {
    "result": "healthy",
    "canary":   { "error_rate": 0.004, "p99_ms": 251, "requests": 8140 },
    "baseline": { "error_rate": 0.003, "p99_ms": 240, "requests": 24390 },
    "evaluated_at": "2026-07-06T14:31:30Z"
  },
  "timeline": [
    { "at": "14:05", "event": "approved by oncall-lead" },
    { "at": "14:06", "event": "step 1: 5% traffic to v2.4.1" },
    { "at": "14:16", "event": "step 1 healthy: promoted to 25%" }
  ]
}

// Side-by-side canary/baseline SLIs from the same minute: the verdict
// is a comparison, not a threshold. Exposure grew to 25% only after
// ten minutes of evidence at 5%.
POST /v1/deployments/:id/rollback (the big red button)
Request
{
  "reason": "checkout_completion_rate dropping on canary"
}
Response
{
  "deployment_id": "dep_8f3e2d1c",
  "status": "rolled_back",
  "traffic_restored_in_seconds": 8,
  "restored_version": "v2.3.8",
  "evidence": {
    "trigger": "manual",
    "last_verdicts": [
      { "result": "breach",
        "detail": "checkout_completion_rate 91.2% vs baseline 96.8%" }
    ]
  }
}

// Eight seconds, because rollback is a traffic-weight change - the
// baseline never stopped running. No approval was requested: rolling
// forward can wait on humans; rolling back never does. The automated
// path (two consecutive breach verdicts) produces this same response
// with trigger: "health-evaluator".
A stored health verdict (the flight-recorder entry)
Response
{
  "verdict_id": 918224,
  "deployment_id": "dep_previous",
  "step": 1,
  "evaluated_at": "2026-07-02T09:14:00Z",
  "canary":   { "error_rate": 0.021, "p99_ms": 512, "requests": 6120 },
  "baseline": { "error_rate": 0.003, "p99_ms": 244, "requests": 118240 },
  "result": "breach",
  "detail": "error_rate 2.1% exceeds baseline 0.3% + 0.5pt margin (window 2 of 2) -> rollback"
}

// Why store every one: this row IS the answer to the 2am question
// "why did it roll back?" - and hundreds of these across the fleet
// are how bake times and margins get tuned from evidence instead of
// vibes. It also documents a catch at 5% that would have been an
// outage at 100%: the system paying for itself.
Error responses (examples)
Response
// 409 - one rollout at a time per service+environment
{
  "error": "deployment_in_progress",
  "message": "payment-service is mid-rollout in production (dep_8f3e2d1c). Pause or complete it first."
}

// 403 - deployment window closed (approval exists, window does not)
{
  "error": "outside_window",
  "message": "Production deploys allowed weekdays 09:00-16:00 UTC",
  "next_window_opens": "2026-07-07T09:00:00Z"
}
// Note what has NO error case: rollback. It is valid in every state,
// in every window, for every caller with access to the service.

💡 Three terms from the flows: bake time, baseline comparison, expand-migrate-contract

Bake time: the deliberate waiting period at each canary step where nothing is promoted and everything is measured. Its length is a tradeoff between detection power and deploy speed: ten minutes catches error spikes and latency regressions, but not a slow memory leak or a code path that runs nightly - which is why bake times are tuned from verdict history, and why feature flags exist as the layer for what canary cannot see. Baseline comparison: judging the canary against the old version serving the same traffic at the same moment, rather than against an absolute threshold. It turns the rollout into a controlled experiment - the baseline is the control group - and automatically cancels out confounders like time-of-day, traffic mix, and upstream slowness that break absolute thresholds. The corollary: keep the baseline running and receiving traffic throughout the entire rollout. Expand-migrate-contract: the three-deploy discipline for schema changes. Expand (add the new shape; old code ignores it), migrate (new code writes both, reads new; backfill), contract (remove the old shape - in a later deploy, only after nothing that needs it will ever run again). It exists because traffic rolls back in seconds and schemas do not roll back at all, and because canary runs two versions against one database simultaneously, making N/N-1 compatibility a structural requirement of the strategy, not a nicety.

Concluding Discussion

Potential Bottlenecks

  • Issue: Release trains - half the org deploys Monday at 10am, and thousands of concurrent rollouts pile up on the metrics platform and the approval queue.
    Mitigation: per-(service, environment) serialization with unlimited cross-service parallelism, staggered windows per team, and the comfort of the estimates: the control plane itself barely notices.
  • Issue: Metrics gaps or lag leave the health evaluator flying blind mid-rollout.
    Mitigation: fail safe, explicitly - insufficient_data is never treated as healthy; the rollout holds its current (safe, static) traffic split, extends the bake, and pages a human rather than promoting on silence.
  • Issue: A shared platform service (auth, a common library service) canaries "healthy" by its own SLIs while quietly degrading its CALLERS.
    Mitigation: platform-critical services gate on downstream SLIs too (their callers' error rates), ramp slower, and bake longer - blast radius includes everyone who depends on you.

Single Points of Failure

  • The deploy system during an incident - the reliability inversion: deploys are the repair path, so this system runs on isolated infrastructure and never on the clusters it deploys to (a bad deploy must not be able to kill the doctor). And a break-glass procedure exists and is REHEARSED: a documented manual traffic shift at the load balancer for the day the deployer itself is down.
  • The metrics platform: our eyes. Its outage does not break serving traffic - rollouts freeze in place (a static split is safe), automated promotion stops, and continuing requires an explicit human override with a second approver. Blind automation is worse than paused automation.
  • The control-plane PostgreSQL: hot standby with failover. In-flight rollouts freeze at their current traffic weights during the gap - the failure mode is "deploys pause," never "traffic moves without a decision-maker."

Security Improvements

  • Supply chain: deploy only what CI provably built - checksums verified on receipt, provenance recorded, and (stronger) artifact signatures checked at deploy time. An attacker who can inject an artifact owns production.
  • Separation of duties: production approval requires a human, policy can require approver ≠ initiator, and every identity lands in the append-only audit log.
  • Rollback authority is deliberately broad while deploy authority is narrow - the asymmetry means safety never waits on privilege escalation.
  • Secrets are injected at deploy time from the secrets manager, scoped per environment, never baked into artifacts or stored in config rows (the same discipline as Design a CI System).

Critical Knowledge

If the interviewer asks "canary or blue-green?":

  • Give a default with a defense, not "it depends." Canary: measures on real traffic BEFORE committing everyone, costs ~5% extra capacity, keeps rollback a seconds-fast traffic shift throughout - the strategy-shaped version of blast-radius control.
  • Blue-green: instant total cutover and instant total rollback, at 100% duplicate capacity and all-or-nothing exposure with no evidence gathered on the way - right where half-and-half operation is impossible (a payment-provider cutover).
  • Rolling: cheapest, no gates, rollback = redeploy - fine for dev/staging where the blast radius is developers.
  • And the trap that catches people: blue-green does NOT escape the database problem - two colors, one database, so N/N-1 schema compatibility binds every strategy equally.

If the interviewer asks "how does the system decide to roll back on its own?":

  • The comparison: canary versus baseline on the same traffic at the same minute - a controlled experiment with the old version as control group - because absolute thresholds break on time-of-day and traffic mix.
  • The trigger: two consecutive breach windows, not one blip (flap protection), evaluated every 30 seconds → detection within ~60-90s, restoration seconds later since the baseline never stopped running.
  • The honesty rules: a min-sample guard (too few canary requests → extend the bake, never promote on noise) and insufficient_data ≠ healthy (no data is never good news).
  • The asymmetries that make it trustworthy: rollback needs no approval ever, and every verdict is stored - automated decisions must show their work, or engineers disable the automation.

If the interviewer asks "what do feature flags add that canary does not?":

  • Different lever for a different failure class: canary catches "this BINARY is bad" (errors, latency, crashes) during the rollout window; flags catch "this FEATURE is bad" (broken flow, bad business metric) - including problems that surface days after 100%.
  • The decoupling: deploy = bytes running, release = behavior experienced. Ship dark, release gradually by cohort, kill instantly by toggle - no pipeline, no traffic shift.
  • Together they layer: canary protects the act of deploying; flags protect the act of releasing; rollback and flag-off are two undo buttons for two different mistakes.
  • The debt honesty: flags accumulate - dead flags breed untested combinations - so every flag ships with an owner and a removal date, or the safety mechanism becomes the hazard.

If the interviewer asks "why are database migrations the scary part?":

  • Because the safety net has a hole exactly there: traffic rolls back in seconds, schemas do not roll back at all - a deploy that dropped a column breaks the very version you roll back to.
  • And canary makes compatibility structural, not optional: old and new versions run simultaneously against ONE database during every rollout, so N/N-1 schema compatibility is a precondition of the strategy.
  • The discipline: expand (add new shape, deploy), migrate (write both/read new, backfill, bake), contract (remove old shape in a LATER deploy, once nothing that needs it can ever run).
  • The one-line rule: never couple a destructive migration to the deploy that needs it - destructive changes ride alone, behind the fleet.

If the interviewer asks "what if the deploy system itself is down during an incident?":

  • Name the inversion first: deployments are how incidents get fixed, so the deploy system's reliability requirement EXCEEDS the products it deploys - the doctor cannot be one of the patients.
  • Structural independence: it runs on isolated infrastructure, never on the clusters it deploys to, so a bad deploy cannot take out the repair path.
  • Fail-safe states everywhere: every failure mode (metrics gone, DB failover, worker crash) freezes traffic at its current static split - deploys pause; traffic never moves without a decision-maker.
  • And the unglamorous answer interviewers respect: a break-glass runbook - a documented, REHEARSED manual traffic shift at the load balancer - because the last resort only works if someone has actually done it before the night it is needed.