Backend / CI/CD / 05_deployment_strategies.md

Deployment Strategies

Updated 7 interview angles 9 min read source
On this page15
  1. The strategies
  2. Recreate — the simplest
  3. Rolling deployment
  4. Blue-green
  5. Canary deployment
  6. A/B testing — canary with a different goal
  7. Shadow deployment (dark launch)
  8. Feature flags — decouple deploy from release
  9. Layered strategies
  10. Database migrations — the hard part
  11. Rollback strategy
  12. Smoke tests post-deploy
  13. Common pitfalls
  14. Common interview confusions
  15. Interview angle

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

text
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:

text
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.

yaml
# 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.

text
[ 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.

text
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.
yaml
# 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.

text
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.

python
if features.is_enabled("new_checkout", user):
    return new_checkout_flow(user)
return old_checkout_flow(user)
text
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.

text
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)

text
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 Zero-downtime migrations.

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:

yaml
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 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 7

  • “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).