Deployment Strategies for Production Systems
A complete guide to deployment strategies in production engineering — covering rolling updates, blue-green, canary, feature flags, and database migration patterns with architecture diagrams and decision frameworks.
Introduction
Deploying software to production is one of the highest-risk operations an engineering team performs.
A poorly executed deployment can cause:
- Complete service outages
- Data corruption
- Revenue loss
- Customer data exposure
- Hours of emergency rollback effort
A well-designed deployment strategy eliminates or minimises all of these risks. It allows teams to ship changes frequently, safely, and with confidence — even across systems serving millions of users.
The choice of deployment strategy is a system design decision. It depends on the risk tolerance of the system, the ability to run multiple versions simultaneously, and the speed of rollback required.
What Makes a Deployment Strategy Production-Ready
A production-grade deployment strategy must satisfy:
| Property | What It Means |
|---|---|
| Zero downtime | Users experience no service interruption during a release |
| Fast rollback | A bad release can be reversed in minutes, not hours |
| Incremental exposure | Changes reach a small subset of users before full rollout |
| Observable | Metrics and errors are monitored throughout the rollout |
| Database compatible | Schema changes are backward compatible with the running version |
| Automated | Deployment steps are executed by a pipeline, not manually |
Deployment Strategy Overview
flowchart TB
Strategies["Deployment Strategies"]
Strategies --> Recreate["Recreate\n(Stop all → Deploy all)"]
Strategies --> Rolling["Rolling Update\n(Instance by instance)"]
Strategies --> BlueGreen["Blue-Green\n(Full parallel environment)"]
Strategies --> Canary["Canary\n(Gradual traffic shift)"]
Strategies --> Feature["Feature Flags\n(Code deployed, feature toggled)"]
Recreate --> R1["❌ Downtime\n✅ Simple"]
Rolling --> R2["✅ No downtime\n✅ Resource efficient"]
BlueGreen --> R3["✅ Instant rollback\n⚠️ Double resources"]
Canary --> R4["✅ Risk controlled\n⚠️ Complex routing"]
Feature --> R5["✅ Deploy and release decoupled\n⚠️ Flag management overhead"]
Strategy 1: Recreate Deployment
How It Works
All running instances are stopped first. Then the new version is deployed.
flowchart LR
subgraph Before["Before Deployment"]
V1A["Instance: v1"]
V1B["Instance: v1"]
V1C["Instance: v1"]
end
subgraph During["During Deployment"]
Down["⛔ All Instances Stopped\n(Service Unavailable)"]
end
subgraph After["After Deployment"]
V2A["Instance: v2"]
V2B["Instance: v2"]
V2C["Instance: v2"]
end
Before --> During --> After
When to Use
- Development and staging environments
- Applications that cannot run two versions simultaneously (tight DB schema coupling)
- Systems with a maintenance window (batch jobs, scheduled processors)
- Very early-stage products with no SLA requirements
Trade-offs
| Advantage | Disadvantage |
|---|---|
| Simplest to implement | Guaranteed downtime during deployment |
| No version conflicts | Users are impacted on every release |
| Clean state guaranteed | Rollback also causes downtime |
Not acceptable for production systems with availability requirements.
Strategy 2: Rolling Update
How It Works
Instances are updated one at a time (or in small batches). At any point during the deployment, both the old and new versions are running simultaneously.
flowchart LR
LB["Load Balancer"]
subgraph Step1["Step 1 — Start"]
A1["v1"]
A2["v1"]
A3["v1"]
A4["v1"]
end
subgraph Step2["Step 2 — Rolling"]
B1["v2"]
B2["v2"]
B3["v1"]
B4["v1"]
end
subgraph Step3["Step 3 — Complete"]
C1["v2"]
C2["v2"]
C3["v2"]
C4["v2"]
end
Step1 --> Step2 --> Step3
LB --> Step1
Traffic is only sent to healthy instances at each step. An instance is only updated after the previous one passes its readiness probe.
Rolling Update in Kubernetes
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
| Parameter | Meaning |
|---|---|
maxUnavailable |
Maximum number of pods that can be unavailable at once |
maxSurge |
Maximum extra pods that can be created above desired count |
With maxUnavailable: 1 and maxSurge: 1 on a 4-pod deployment:
- A fifth pod (v2) starts before any v1 pod is removed
- One v1 pod is removed only after the new v2 pod passes readiness
- This ensures there are always at least 3 healthy pods throughout
Version Compatibility Requirement
Because v1 and v2 run simultaneously during a rolling update:
- The API must be backward compatible — v2 must accept v1-format requests
- The database schema must be backward compatible — v2 schema additions must not break v1 queries
- Message formats must remain compatible — old consumers must still process new message shapes
flowchart LR
Client --> LB["Load Balancer"]
LB --> Pod1["Pod: v1"]
LB --> Pod2["Pod: v1"]
LB --> Pod3["Pod: v2"]
LB --> Pod4["Pod: v2"]
Pod1 --> DB["Shared Database\n(must support both versions)"]
Pod2 --> DB
Pod3 --> DB
Pod4 --> DB
When to Use
- Standard production deployments with no strict rollback time requirement
- Services with backward-compatible API and database changes
- Kubernetes-native workloads
Trade-offs
| Advantage | Disadvantage |
|---|---|
| No downtime | Two versions run simultaneously |
| Resource efficient (no extra infra) | Rollback is another rolling update (takes time) |
| Simple Kubernetes configuration | API and schema must be backward compatible |
Strategy 3: Blue-Green Deployment
How It Works
Two identical environments — Blue (current live) and Green (new version) — exist simultaneously. All traffic runs on Blue. Green is deployed and tested in isolation. When ready, all traffic is switched from Blue to Green in a single step.
flowchart TB
DNS["DNS / Load Balancer\n(Traffic Switch)"]
subgraph Blue["Blue Environment (Live — v1)"]
B1["Pod: v1"]
B2["Pod: v1"]
B3["Pod: v1"]
end
subgraph Green["Green Environment (Staging — v2)"]
G1["Pod: v2"]
G2["Pod: v2"]
G3["Pod: v2"]
end
DNS -->|"100% traffic"| Blue
DNS -.->|"0% traffic\n(ready to switch)"| Green
After the switch:
flowchart TB
DNS["DNS / Load Balancer\n(After Switch)"]
subgraph Blue["Blue Environment (Idle — v1)"]
B1["Pod: v1"]
B2["Pod: v1"]
end
subgraph Green["Green Environment (Now Live — v2)"]
G1["Pod: v2"]
G2["Pod: v2"]
G3["Pod: v2"]
end
DNS -.->|"0% traffic\n(kept for rollback)"| Blue
DNS -->|"100% traffic"| Green
Rollback
If Green has a problem, rollback is a single DNS or load balancer switch back to Blue — typically completing in under 60 seconds.
sequenceDiagram
participant Team
participant LoadBalancer
participant Green
participant Blue
Team->>LoadBalancer: Switch traffic to Green
LoadBalancer-->>Green: 100% traffic
Note over Green: Error rate spikes
Team->>LoadBalancer: Rollback — switch to Blue
LoadBalancer-->>Blue: 100% traffic
Note over Blue: Service fully restored in < 60s
Database Considerations
Blue-Green requires both versions to share the same database during the cutover window, which means the same backward compatibility constraints as rolling updates apply.
flowchart LR
Blue["Blue Pods (v1)"] --> DB["Shared Database"]
Green["Green Pods (v2)"] --> DB
After the switch and a stabilization period, the old Blue environment can be scaled down and the database migrated to the v2 schema.
When to Use
- High-value releases where instant rollback is required
- Major version changes that are hard to test incrementally
- Regulated systems (financial, healthcare) requiring a clean cutover
- Systems where testing the full environment before go-live is mandatory
Trade-offs
| Advantage | Disadvantage |
|---|---|
| Instant rollback (< 60 seconds) | Doubles infrastructure cost during deployment |
| Zero user impact on cutover | Database changes require backward compatibility |
| Full environment tested before go-live | Traffic switch is all-or-nothing |
| Clean version separation | More complex infrastructure management |
Strategy 4: Canary Deployment
How It Works
A small percentage of traffic is routed to the new version. The majority continues on the current version. The new version is monitored closely. If metrics are healthy, traffic is gradually shifted until the canary reaches 100%.
flowchart LR
Users["All Users\n(1000 req/s)"]
LB["Load Balancer\n(Traffic Splitting)"]
V1["Stable: v1\n90% traffic → 900 req/s"]
V2["Canary: v2\n10% traffic → 100 req/s"]
Users --> LB
LB -->|"90%"| V1
LB -->|"10%"| V2
Canary Rollout Progression
flowchart LR
S1["Stage 1\nCanary: 5%\nStable: 95%"]
S2["Stage 2\nCanary: 20%\nStable: 80%"]
S3["Stage 3\nCanary: 50%\nStable: 50%"]
S4["Stage 4\nCanary: 100%\nOld: 0%"]
S1 -->|"Metrics healthy"| S2
S2 -->|"Metrics healthy"| S3
S3 -->|"Metrics healthy"| S4
S1 -->|"Error spike"| Rollback["Rollback to 0%"]
S2 -->|"Error spike"| Rollback
S3 -->|"Error spike"| Rollback
Each stage is held for a defined soak time (e.g., 15–30 minutes) while metrics are observed. Automation can promote or rollback based on SLO thresholds.
What to Monitor During a Canary
| Signal | Threshold to Watch |
|---|---|
| Error rate | Canary error rate vs stable |
| p99 latency | Canary latency vs stable |
| Business metrics | Order completion, payment success |
| CPU / Memory | Resource consumption per pod |
If the canary's error rate exceeds the stable baseline by more than a defined threshold, the deployment is automatically rolled back to 0%.
Canary in Kubernetes with Argo Rollouts
strategy:
canary:
steps:
- setWeight: 5
- pause:
duration: 15m
- setWeight: 20
- pause:
duration: 15m
- setWeight: 50
- pause:
duration: 15m
- setWeight: 100
analysis:
templates:
- templateName: error-rate-check
args:
- name: service-name
value: payment-service
When to Use
- High-traffic services where a bad deployment affects many users
- Changes to core business logic (payments, orders, authentication)
- Releases that need real traffic validation before full rollout
- Systems with mature monitoring and SLO dashboards in place
Trade-offs
| Advantage | Disadvantage |
|---|---|
| Limits blast radius of bad releases | Two versions run simultaneously |
| Detects real-world issues early | Requires traffic splitting infrastructure |
| Automated rollback on SLO breach | Users may experience inconsistent behaviour |
| Gradual confidence building | Canary analysis setup requires investment |
Strategy 5: Feature Flags
How It Works
Code is deployed to production but the feature is switched off. Features are enabled independently from deployment — by toggling a flag in a configuration store, without any new deployment.
flowchart LR
Deploy["Deploy v2\n(Feature code present\nbut flag OFF)"]
Deploy --> Prod["Production\n(v2 running, feature hidden)"]
Prod --> Toggle["Enable flag\nfor 1% of users"]
Toggle --> Rollout["Gradually increase\nto 100%"]
Toggle -->|"Problems"| Disable["Disable flag instantly\n(no deployment needed)"]
Feature Flag Architecture
flowchart TB
App["Application"] --> FlagClient["Feature Flag Client\n(LaunchDarkly / Unleash / Flipt)"]
FlagClient --> FlagStore["Flag Store\n(Remote config)"]
FlagStore --> Rules["Targeting Rules\n(user ID, region, % rollout)"]
Rules --> Decision{"Flag ON\nor OFF?"}
Decision -->|"ON"| NewCode["Execute new feature code"]
Decision -->|"OFF"| OldCode["Execute existing code path"]
Targeting Strategies
Feature flags enable precise control over who sees a feature:
| Strategy | Example |
|---|---|
| User segment | Enable for internal employees only |
| Percentage rollout | Enable for 5% of users randomly |
| Region | Enable in US before EU |
| Account tier | Enable for premium users first |
| A/B testing | Split traffic to measure conversion impact |
Deployment vs Release
Feature flags decouple the deployment from the release:
flowchart LR
Code["Code merged\nto main"] --> Deploy["Deploy to production\n(flag OFF)"]
Deploy --> Verify["Verify system\nstability"]
Verify --> Release["Enable flag\nfor target users"]
Release --> Monitor["Monitor metrics"]
Monitor -->|"Issue"| Disable["Disable flag\n(instant rollback)"]
Monitor -->|"Healthy"| Expand["Expand to 100%"]
This means a deployment is a low-risk infrastructure operation. The release is a separate, controlled business decision.
When to Use
- Long-running feature development that merges incrementally to main
- A/B testing and experimentation
- Kill switches for third-party integrations
- Gradual rollout to user segments without infrastructure routing changes
- Emergency disable capability for new features
Trade-offs
| Advantage | Disadvantage |
|---|---|
| Instant rollback without a deployment | Flag debt accumulates if not cleaned up |
| Deploy and release are independent | Code has conditional branches long-term |
| Precise targeting (user, region, %) | Requires flag management infrastructure |
| Enables safe trunk-based development | Testing all flag combinations adds complexity |
Deployment Strategy Comparison
| Strategy | Downtime | Rollback Speed | Resource Cost | Version Overlap | Complexity |
|---|---|---|---|---|---|
| Recreate | Yes | Fast (redeploy) | Low | None | Low |
| Rolling Update | No | Slow (re-roll) | Low | Yes | Low |
| Blue-Green | No | Instant (< 60s) | 2x during deploy | Short window | Medium |
| Canary | No | Instant (0%) | Low overhead | Yes | High |
| Feature Flags | No | Instant (toggle) | None | Yes (in code) | Medium |
Database Migration Patterns
Every deployment strategy must address database schema changes. Schema migrations are the most common cause of deployment failures.
The Core Challenge
flowchart LR
OldApp["v1 App\n(running during migration)"]
NewApp["v2 App\n(needs new schema)"]
DB["Database\n(shared)"]
OldApp --> DB
NewApp --> DB
If v1 and v2 run simultaneously, the database must be compatible with both versions.
Expand and Contract Pattern
The safest approach to schema changes in zero-downtime deployments.
flowchart LR
Phase1["Phase 1: Expand\nAdd new column\n(nullable, no default constraint)\nBoth v1 and v2 work"]
Phase2["Phase 2: Migrate\nBackfill existing rows\nv2 writes to both old and new column"]
Phase3["Phase 3: Contract\nRemove old column\nOnly after v1 is fully retired"]
Phase1 --> Phase2 --> Phase3
Example — renaming a column:
| Phase | Schema State | App Versions Supported |
|---|---|---|
| Expand | Both customer_name and full_name exist |
v1 (reads old), v2 (reads new) |
| Migrate | Backfill full_name from customer_name |
v1 and v2 both work |
| Contract | Remove customer_name |
v2 only |
Rules for Safe Schema Migrations
| Operation | Safe in Zero-Downtime? | Notes |
|---|---|---|
| Add nullable column | ✅ Yes | Old app ignores new column |
| Add column with default | ✅ Yes | Old app ignores new column |
| Add index (concurrent) | ✅ Yes | Use CREATE INDEX CONCURRENTLY |
| Add new table | ✅ Yes | Old app is unaffected |
| Rename column | ❌ No (directly) | Use expand and contract |
| Drop column | ❌ No (directly) | Only after all app versions stop using it |
| Change column type | ❌ No (directly) | Use expand and contract with a new column |
| Add NOT NULL constraint | ❌ No (directly) | Backfill nulls first, then add constraint |
CI/CD Pipeline Integration
A deployment strategy is only as good as the pipeline that executes it. Every production deployment should flow through an automated pipeline.
flowchart LR
Code["Code Merged\nto Main"]
Code --> Build["Build\n(Compile + Test)"]
Build --> UnitTests["Unit Tests\n+ Integration Tests"]
UnitTests --> ContainerBuild["Container Image Build\n(Docker)"]
ContainerBuild --> SecurityScan["Security Scan\n(Image vulnerabilities)"]
SecurityScan --> PushRegistry["Push to\nContainer Registry"]
PushRegistry --> DeployStaging["Deploy to Staging\n(Rolling / Blue-Green)"]
DeployStaging --> SmokeTests["Smoke Tests\n(Automated)"]
SmokeTests --> DeployProd["Deploy to Production\n(Canary / Blue-Green)"]
DeployProd --> Monitor["Monitor SLOs\n(Auto-rollback if breach)"]
Pipeline Gates
Each stage is a gate. A failure at any stage stops the pipeline and prevents the change from reaching production.
| Gate | What It Checks |
|---|---|
| Unit tests | Individual component correctness |
| Integration tests | Component interaction and API contracts |
| Security scan | Known CVEs in dependencies and base images |
| Staging deploy | Change deploys successfully in a production-like environment |
| Smoke tests | Critical user journeys work end-to-end |
| SLO monitoring | Production metrics stay within acceptable bounds |
Choosing the Right Strategy
flowchart TD
Q1{"Is downtime acceptable?"}
Q1 -- Yes --> Recreate["Recreate\n(Simple, full restart)"]
Q1 -- No --> Q2{"Need instant rollback?"}
Q2 -- Yes --> Q3{"Can afford double infra?"}
Q3 -- Yes --> BlueGreen["Blue-Green\n(Instant switch, full env)"]
Q3 -- No --> Q4{"High traffic, need risk control?"}
Q4 -- Yes --> Canary["Canary\n(Gradual traffic shift)"]
Q4 -- No --> Rolling["Rolling Update\n(Default for most services)"]
Q2 -- No --> Q5{"Decouple deploy from release?"}
Q5 -- Yes --> FeatureFlags["Feature Flags\n(Toggle without deployment)"]
Q5 -- No --> Rolling
| Scenario | Recommended Strategy |
|---|---|
| Standard microservice update | Rolling Update |
| Major version with complex rollback requirements | Blue-Green |
| Core payment or auth service change | Canary |
| Large feature developed over multiple sprints | Feature Flags |
| Batch job or scheduled processor | Recreate |
| Experiment or A/B test | Feature Flags |
Production Deployment Checklist
Before executing any production deployment:
Pre-Deployment
- All tests pass in CI (unit, integration, smoke)
- Container image scanned for vulnerabilities
- Database migrations reviewed for backward compatibility
- Rollback plan documented and tested in staging
- Monitoring dashboards open and baseline captured
- On-call engineer notified and available
During Deployment
- Error rate monitored continuously
- p99 latency compared to pre-deployment baseline
- Business metrics (orders, payments) tracked in real time
- Readiness probes passing on new instances before traffic shift
Post-Deployment
- SLO compliance confirmed for 15–30 minutes post-deploy
- Logs checked for new error patterns
- Database migration completed and verified
- Old environment (blue) retained for rollback window (minimum 1 hour)
- Deployment documented in change log
Summary
Deployment strategy is a production system design decision — not just a DevOps concern. The right strategy depends on availability requirements, rollback speed, resource constraints, and the nature of the change being shipped.
| Strategy | Best For | Key Requirement |
|---|---|---|
| Recreate | Non-production, maintenance windows | Acceptable downtime |
| Rolling | Standard service updates | Backward-compatible changes |
| Blue-Green | High-stakes releases, regulated systems | Double infrastructure |
| Canary | High-traffic critical services | Traffic splitting + monitoring |
| Feature Flags | Long-lived features, experiments, kill switches | Flag management tooling |
The most mature engineering teams combine strategies — rolling updates for infrastructure changes, canary for logic changes, and feature flags for product rollouts. Every critical service should have a documented deployment strategy before going to production.