Deployment Strategies
How code goes from CI artifact to production. The patterns trade off risk, complexity, and rollback speed. Interview questions ask “which would you pick?” — answer with trade-offs, not “blue-green is always better.”
The strategies
| Strategy | Risk | Rollback | Complexity |
|---|---|---|---|
| Recreate | high — downtime | redeploy old | trivial |
| Rolling | medium | redeploy old | low |
| Blue-green | low | flip switch | medium |
| Canary | very low | reduce canary % | medium-high |
| A/B testing | controlled | feature flag | high (analytics) |
| Shadow | none (no user impact) | turn off shadow | high |
| Feature flags | zero deploy risk | toggle off | medium |
For most apps: rolling. For higher-stakes: canary. Feature flags layered on top for safety.
Recreate — the simplest
Stop all old instances → start all new instances
Downtime during the gap. Used for:
- Stateful migrations where old and new can’t coexist.
- Dev environments.
- Some database deploys.
Don’t use for user-facing services unless downtime is acceptable.
Rolling deployment
Replace instances gradually:
Time 0: [old] [old] [old] [old]
Time 1: [old] [old] [old] [NEW]
Time 2: [old] [old] [NEW] [NEW]
Time 3: [old] [NEW] [NEW] [NEW]
Time 4: [NEW] [NEW] [NEW] [NEW]
Most orchestrators default to rolling (Kubernetes Deployment, ECS, Nomad). Configurable batch size and health checks between batches.
# Kubernetes Deployment
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # at most 1 pod down at a time
maxSurge: 1 # at most 1 extra pod beyond desired count
Trade-offs:
- No downtime if both versions can coexist.
- Standard, well-tooled.
- Brief period where requests hit both versions — backwards compatibility needed.
- Slow rollback (must roll the new version back through the same gradual process).
Compatibility requirement: new and old code must coexist during the transition. Database schema changes need to be backwards-compatible (add columns, don’t rename).
Blue-green
Two complete environments. Switch traffic between them.
[ Blue (live) ] ← traffic
[ Green (idle) ]
→ Deploy new version to Green
[ Blue (live) ] ← traffic
[ Green (new, idle) ]
→ Switch traffic
[ Blue (old, idle) ]
[ Green (live) ] ← traffic
→ Validate Green, decommission Blue
The “switch” is typically a load balancer change (point at the other target group) or a DNS update.
Trade-offs:
- Instant cutover; instant rollback (flip back).
- Validate new version with smoke tests before switching.
- 2× infrastructure cost during transition.
- Database changes still need backwards compatibility (new code runs against the same DB the old will need if you rollback).
- In-flight connections: WebSocket / long-poll connections to old code stay until they close.
Good for: critical services where rollback speed matters more than infrastructure cost. Less common at very large scale (running 2× capacity is expensive).
Canary deployment
Roll out to a small percentage of users / hosts first. Watch metrics. If healthy, expand.
Time 0: 100% of traffic → old
Time 1: 1% → canary, 99% → old
Time 2: 5% → canary, 95% → old
Time 3: 25% → canary, 75% → old
Time 4: 100% → canary (now the new version)
Implementations:
- Service mesh (Istio, Linkerd) — traffic-split rules.
- Kubernetes + Argo Rollouts / Flagger — automated canary controller.
- Load balancer with weighted targets — AWS ALB, GCP LB.
- Feature flags at the application layer — gradual rollout per user / region.
Health check during canary:
- Error rate.
- Latency.
- Custom business metrics (conversion rate, etc.).
- Compare canary vs baseline statistically.
# Argo Rollouts canary
spec:
strategy:
canary:
steps:
- setWeight: 5
- pause: { duration: 10m }
- analysis:
templates: [{name: success-rate}]
- setWeight: 25
- pause: { duration: 10m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
If analysis fails at any step, auto-rollback.
Trade-offs:
- Limits blast radius of bad deploys.
- Real production validation before full rollout.
- Needs good metrics + automated analysis.
- Both versions running for the duration.
- Complex when state is involved (cache, session) — users may bounce between versions.
For high-traffic critical services: canary is the standard.
A/B testing — canary with a different goal
Same mechanics as canary (traffic split) but the goal is measuring user behavior, not validating deploys:
| Canary | A/B test |
|---|---|
| Goal: detect regressions | Goal: compare metrics (conversion, engagement) |
| Duration: minutes to hours | Duration: days to weeks |
| Decision: promote or rollback | Decision: pick the winning variant |
| Metrics: technical (errors, latency) | Metrics: business (revenue, retention) |
Often combined with feature flags so each user sees a consistent variant.
Shadow deployment (dark launch)
Send a copy of production traffic to the new version. Don’t return its responses to users.
User → [LB] → [old version] → response to user
[new version (shadow)] → discarded
Use cases:
- Test performance under real load.
- Verify new version handles real traffic patterns.
- Migrate a major dependency (new DB, new search engine).
Implementation: load balancer / service mesh duplicates requests. Or instrumented code does the same work twice and compares.
Trade-offs:
- Zero user impact even if new version is broken.
- Real-world testing.
- 2× load on infrastructure during shadow.
- Side effects (writes, external calls) — must be carefully handled. Usually: shadow reads only, or sandbox the writes.
- Comparison complexity.
For high-stakes migrations (payment systems, search), shadow is invaluable.
Feature flags — decouple deploy from release
Code ships behind a flag, disabled. Release = flip the flag.
if features.is_enabled("new_checkout", user):
return new_checkout_flow(user)
return old_checkout_flow(user)
Deploy: ship code, flag off → no user impact
Release: turn flag on for 5% of users → measure
turn flag on for 25% → measure
turn flag on for 100% → done
Rollback: turn flag off → instant
Trade-offs:
- Deploy and release are independent.
- Instant rollback (no redeploy).
- Granular: per-user, per-tenant, per-region rollout.
- Combine with canary for layered safety.
- Flag debt: dead code behind flags accumulates.
- Testing: must test both branches.
- Runtime overhead (flag evaluation).
Tools: LaunchDarkly, Unleash, Statsig, Flipt, Split.io, or your own DB-backed implementation.
The modern best practice: feature flags + small frequent merges to main. Trunk-based development becomes safe because incomplete features ship behind flags.
Layered strategies
Real production: multiple of these combine.
PR merged → CI passes → deploy to staging (rolling)
↓ smoke tests
deploy to production canary 5%
↓ analyze 10 min
promote to 50%
↓ analyze 10 min
promote to 100% (rolling within prod)
↓
feature flag still off
↓
turn flag on 1% → 5% → ... → 100%
Three layers: canary deploy, rolling deploy, feature flag rollout. Each catches different categories of issues.
Database migrations — the hard part
Application code can blue-green. The database can’t (easily).
Patterns for zero-downtime schema changes:
Expand-contract (parallel change)
Phase 1 (expand): add new column, keep old
Phase 2: new code writes to BOTH old and new
Phase 3: backfill old data into new column
Phase 4 (contract): new code reads from new column only
Phase 5: stop writing to old column
Phase 6: drop old column
Each phase is independently deployable. Old code keeps working throughout.
Backwards-compatible only
Allowed:
- Add nullable column.
- Add index.
- Add new table.
Not allowed in one step:
- Rename column.
- Drop column still referenced by old code.
- Change column type incompatibly.
- Add NOT NULL without default.
The deploy pipeline enforces this — migrations can only contain backwards-compatible changes. The “drop old column” lands later, after the old code is fully retired.
See ../08_databases/sql/13_zero_downtime_migrations.md.
Rollback strategy
Every deploy strategy needs a rollback plan:
| Strategy | Rollback |
|---|---|
| Recreate | redeploy previous version (downtime again) |
| Rolling | rolling deploy back to old version (slow) |
| Blue-green | flip the LB pointer back (instant) |
| Canary | reduce canary weight to 0, eventually scale down |
| Feature flag | toggle off (instant, no redeploy) |
For mission-critical: combine blue-green / canary (fast infrastructure rollback) with feature flags (fast logic rollback). Belt and suspenders.
Important: practice rollbacks. The first time you need to rollback in prod should not be the first time you’ve done it. Quarterly fire drills.
Smoke tests post-deploy
Don’t just deploy and hope. Run synthetic tests against the deployed version:
deploy_production:
script: kubectl apply -f manifests/
after_script: |
# Wait for rollout to complete
kubectl rollout status deployment/api -w
# Run smoke tests against production
curl --fail https://api.example.com/health
pytest tests/smoke/ --base-url https://api.example.com
If smoke tests fail → automatic rollback. Some orchestrators (Argo Rollouts) integrate this natively as “analysis steps.”
Common pitfalls
- No health checks during rolling deploy — old pods drain mid-request; new pods aren’t ready yet. Set liveness/readiness probes.
- Blue-green with shared DB and incompatible schema — Green expects new schema; Blue expects old. Switch the LB → Blue breaks. Use expand-contract.
- Canary % too aggressive — 50% canary skips meaningful sample of 5%. Start small.
- Canary metrics that don’t differ at small percentages — 1% of traffic may not generate enough signal in 10 min. Adjust thresholds.
- Feature flag forever — flag turned on, never cleaned up. Tech debt. Schedule cleanup sprints.
- No deploy-time rollback drill — first rollback is the worst time to discover the runbook is wrong.
- Sticky sessions + new deploy — users pinned to old pods don’t see the new version. See ../12_protocols/nginx/.
Common interview confusions
- “Blue-green is the same as canary.” — blue-green is all-or-nothing flip. Canary is gradual percentage rollout. Different risk profiles.
- “Rolling deploys have no risk.” — bad version rolls out gradually; eventually all pods are bad. Health checks help but won’t catch logic bugs that pass health checks.
- “Feature flags replace deploys.” — they decouple deploy from release. You still deploy; you just don’t expose the change to users until flipped.
Interview angle
- “What’s the difference between blue-green and canary deployments?” — blue-green: two complete environments, switch all traffic at once (fast cutover and rollback, 2× cost). Canary: gradual percentage rollout to subset of users / hosts, observe metrics, expand. Different trade-offs in risk vs cost.
- “How does rolling deployment work?” — replace instances gradually (batches), with health checks between batches. Old and new versions coexist during the rollout. No downtime if they’re backwards-compatible. Standard for Kubernetes Deployment, ECS, etc.
- “When would you use blue-green over rolling?” — when rollback speed matters more than infrastructure cost. Mission-critical services with stable user base where 2× capacity is acceptable.
- “How do canary deployments minimize risk?” — small percentage of real traffic hits the new version. If metrics (errors, latency, business KPIs) regress, rollback before full rollout. Service meshes / Argo Rollouts automate the analysis steps.
- “What’s a feature flag and why pair it with canary?” — feature flag decouples deploy from release. Canary catches deploy-time issues (new code crashes, doesn’t start); feature flags catch logic issues (new behavior is wrong for users). Layered safety.
- “How do you do database migrations without downtime?” — expand-contract: backwards-compatible schema changes first (add column, dual-write, backfill), then deploy code that reads from the new column, finally drop the old column. Multiple deploys, never breaking compatibility.
- “What’s a shadow deployment?” — duplicate real traffic to a new version; discard its responses. Tests performance / correctness against real load without user impact. Used for high-stakes migrations (payments, search). Careful with side effects (writes).