~learn.
?

Day 0340 minPreface+60 XP

Deployment Strategies.

System Design Mastery Series — Production Readiness and Operational Excellence

4 stages12 code

watch it first · newThe whole lesson in a 18-minute animated lecture5 quiz checkpoints · ask the AI about any moment · captions, chapters and speed

Preface · 1 min

Preface#

You can define what healthy means (SLOs). You can see if you're healthy (observability).

Now: how do you ship changes without breaking that health?

The deployment paradox

To improve a system, you must change it. To keep a system stable, you must not change it.

Every deployment is a risk
  • New code might have bugs
  • New config might be wrong
  • New dependencies might fail
  • New scale might break assumptions
  • Humans might make mistakes
Yet we MUST deploy
  • Fix bugs
  • Add features
  • Patch security
  • Improve performance
  • Stay competitive
The solution

Deploy frequently, but deploy SAFELY.

Deployment philosophy
Old way Modern way
Deploy monthly (or quarterly!) Deploy daily (or more!)
Big bang releases Small, incremental changes
"Change freeze" before releases Automated pipelines
All-hands-on-deck deployment days Gradual rollouts with monitoring
Hope nothing breaks Automatic rollback if problems detected

The safest deployment is a small one. The safest rollback is a fast one.

Today, we learn to ship changes without fear.

ExplainerThe whole lesson: deployment strategies

To improve a system you must change it, so deploy frequently but safely. Big bang means downtime and every user on the new version at once; rolling replaces instances gradually but runs two versions together with a slow rollback; blue-green switches all traffic to an idle environment and back instantly, for twice the infrastructure; canary moves from 1% to 10%, 50% and 100%, compared with the stable version at each stage. Feature flags decouple deployment from release and must be removed once at 100%, a column rename runs as expand, migrate and contract so old and new code both keep working, and rollback is instant for a flag, fast for code and config, and slow and risky for data.

Part I · Stage 1 of 4 · 21 min

Part I: Foundations#

Chapter 1: Deployment Strategies Overview#

1.1 The Deployment Strategy Spectrum#

Deployment strategies
From RISK → SAFETY
  1. Big Bang — All at once · High risk · Fast
  2. Rolling — Gradual replacement · Medium risk
  3. Blue-Green — Instant Switchover · Fast rollback
  4. Canary — Gradual % rollout · Safest
  5. Feature Flags — Code live, not active · Decoupled
Strategy comparison
Strategy Downtime Rollback Resource Cost Complexity
Big Bang Yes Full redeploy 1x Low
Rolling No Slow 1x + buffer Medium
Blue-Green No Instant 2x Medium
Canary No Fast 1x + small High
Feature Flag No Instant 1x High

1.2 Big Bang Deployment (Don't Do This)#

Big bang deployment
What it is
  • Stop old version
  • Deploy new version
  • Start new version
  • Hope it works
Timeline

v1.0 Running → Stop v1 → DOWNTIME → Start v1.1 → v1.1 Running

Problems
  • Downtime during deployment
  • All users hit new version at once
  • If broken, ALL users affected
  • Rollback requires another deployment
  • High stress, high risk
When it's acceptable
  • Development environment
  • Non-critical internal tools
  • Scheduled maintenance windows
  • When other options truly impossible

For production services: AVOID.

1.3 Rolling Deployment#

Rolling deployment
What it is
  • Gradually replace old instances with new
  • One (or few) at a time
  • Health checks before proceeding
  • No downtime
Timeline (v1.0 → v1.1, one instance after another)
  1. Instance 1 — switched to v1.1 first
  2. Instance 2 — switched next
  3. Instance 3 — switched next
  4. Instance 4 — switched next
  5. Instance 5 — still on v1.0 until the end of the rollout
Traffic distribution during rollout
Stage v1.0 v1.1
Start 100% 0%
1 of 5 done (20%) 80% 20%
2 of 5 done (40%) 60% 40%
3 of 5 done (60%) 40% 60%
4 of 5 done (80%) 20% 80%
Complete 0% 100%
Advantages
  • No downtime
  • Gradual risk exposure
  • Can pause if issues detected
  • Natural load balancing
Disadvantages
  • Two versions running simultaneously
  • Rollback is slow (reverse the process)
  • Database must support both versions
  • API must be backward compatible

Chapter 2: Blue-Green Deployment#

Your turn · checkpoint 1not answered

Trade-off: instant rollback, at what price?

Your team wants to undo a bad release in seconds, but finance won't fund twice the servers all year. Compare a rolling deployment and a blue-green deployment on rollback speed and cost. Which would you pick, and is there a third option?

Think it through, or jot it down. Then compare.

One strong answer

  • Rolling: about 1× the servers plus a small buffer, but rollback means reversing the rollout instance by instance, which is slow.
  • Blue-green: rollback is one traffic switch back to the old environment, which is still running. The price is 2× the infrastructure.
  • Third option: a canary costs about 1× plus a little, and backing out is fast because only a small slice of traffic ever reached the new version.

2.1 How Blue-Green Works#

DiagramBlue-green deployment architecture
%% title: Blue-green deployment architecture
flowchart TD
  LB@{ icon: "sd:load-balancer", label: "Load Balancer" }
  subgraph BLUE["BLUE (Current)"]
    B1@{ icon: "sd:server", label: "App 1" }
    B2@{ icon: "sd:server", label: "App 2" }
    B3@{ icon: "sd:server", label: "App 3" }
  end
  subgraph GREEN["GREEN (New)"]
    G1@{ icon: "sd:server", label: "App 1" }
    G2@{ icon: "sd:server", label: "App 2" }
    G3@{ icon: "sd:server", label: "App 3" }
  end
  LB -->|"switches between"| BLUE
  LB -.->|"switches between"| GREEN
  BLUE --> DB@{ icon: "sd:database", label: "Shared Database" }
  GREEN --> DB
  class LB,B1,B2,B3 edge
  class G1,G2,G3 ok
  class DB store
Deployment process
  1. CURRENT STATE
    • Blue: Running v1.0, receiving traffic
    • Green: Idle (or running old version)
  2. DEPLOY NEW VERSION
    • Blue: Still running v1.0, receiving traffic
    • Green: Deploy v1.1, run smoke tests
  3. SWITCH TRAFFIC
    • Blue: Running v1.0, NO traffic
    • Green: Running v1.1, receiving ALL traffic
  4. VERIFY
    • Monitor metrics, logs, errors
    • If problems: Switch back to Blue (instant rollback)
  5. CLEANUP
    • Blue becomes the standby for next deployment

2.2 Blue-Green Implementation#

pythonEnvironment · 226 lines
# deployment/blue_green.py

"""
Blue-Green deployment orchestration.

This manages the switch between environments
and provides instant rollback capability.
"""

from dataclasses import dataclass
from enum import Enum
from typing import Optional
from datetime import datetime, timezone
import asyncio
import logging
import uuid

logger = logging.getLogger(__name__)


class Environment(Enum):
    BLUE = "blue"
    GREEN = "green"


class DeploymentState(Enum):
    IDLE = "idle"
    DEPLOYING = "deploying"
    TESTING = "testing"
    SWITCHING = "switching"
    VERIFYING = "verifying"
    COMPLETE = "complete"
    ROLLED_BACK = "rolled_back"


@dataclass
class DeploymentRecord:
    """Record of a deployment."""
    id: str
    version: str
    started_at: datetime
    completed_at: Optional[datetime]
    state: DeploymentState
    active_environment: Environment
    previous_environment: Environment
    deployed_by: str
    rollback_reason: Optional[str] = None


class BlueGreenDeployer:
    """
    Orchestrates blue-green deployments.
    """
    
    def __init__(
        self,
        load_balancer,
        blue_cluster,
        green_cluster,
        health_checker,
        metrics_client
    ):
        self.lb = load_balancer
        self.blue = blue_cluster
        self.green = green_cluster
        self.health = health_checker
        self.metrics = metrics_client
        
        self.current_environment = None
        self.current_deployment = None
    
    async def deploy(
        self,
        version: str,
        deployed_by: str,
        skip_tests: bool = False
    ) -> DeploymentRecord:
        """
        Execute a blue-green deployment.
        """
        # Determine which environment to deploy to
        if self.current_environment == Environment.BLUE:
            target = Environment.GREEN
            target_cluster = self.green
        else:
            target = Environment.BLUE
            target_cluster = self.blue
        
        deployment = DeploymentRecord(
            id=str(uuid.uuid4()),
            version=version,
            started_at=datetime.now(timezone.utc),
            completed_at=None,
            state=DeploymentState.DEPLOYING,
            active_environment=self.current_environment,
            previous_environment=self.current_environment,
            deployed_by=deployed_by
        )
        
        try:
            # Step 1: Deploy to inactive environment
            logger.info(f"Deploying {version} to {target.value}")
            deployment.state = DeploymentState.DEPLOYING
            
            await target_cluster.deploy(version)
            
            # Step 2: Run health checks and tests
            logger.info(f"Running health checks on {target.value}")
            deployment.state = DeploymentState.TESTING
            
            if not skip_tests:
                health_result = await self.health.check_cluster(target_cluster)
                if not health_result.healthy:
                    raise DeploymentError(f"Health check failed: {health_result.reason}")
                
                # Run smoke tests
                test_result = await self._run_smoke_tests(target_cluster)
                if not test_result.passed:
                    raise DeploymentError(f"Smoke tests failed: {test_result.failures}")
            
            # Step 3: Switch traffic
            logger.info(f"Switching traffic to {target.value}")
            deployment.state = DeploymentState.SWITCHING
            
            await self.lb.switch_to(target)
            
            # Step 4: Verify in production
            logger.info("Verifying deployment")
            deployment.state = DeploymentState.VERIFYING
            
            # Wait and check metrics
            await asyncio.sleep(30)  # Give it 30 seconds
            
            verification = await self._verify_deployment(version)
            if not verification.success:
                logger.warning(f"Verification failed: {verification.reason}")
                await self.rollback(deployment, verification.reason)
                return deployment
            
            # Step 5: Complete
            deployment.state = DeploymentState.COMPLETE
            deployment.completed_at = datetime.now(timezone.utc)
            deployment.active_environment = target
            
            self.current_environment = target
            self.current_deployment = deployment
            
            logger.info(f"Deployment {deployment.id} complete")
            
            return deployment
            
        except Exception as e:
            logger.error(f"Deployment failed: {e}")
            await self.rollback(deployment, str(e))
            raise
    
    async def rollback(
        self,
        deployment: DeploymentRecord,
        reason: str
    ):
        """
        Rollback to previous environment.
        
        This is INSTANT because the old environment is still running.
        """
        logger.warning(f"Rolling back deployment {deployment.id}: {reason}")
        
        deployment.state = DeploymentState.ROLLED_BACK
        deployment.rollback_reason = reason
        deployment.completed_at = datetime.now(timezone.utc)
        
        # Switch traffic back to previous environment
        await self.lb.switch_to(deployment.previous_environment)
        
        # Record rollback metric
        self.metrics.increment(
            "deployments_rolled_back_total",
            labels={"version": deployment.version, "reason": reason}
        )
        
        logger.info(f"Rollback complete. Traffic restored to {deployment.previous_environment.value}")
    
    async def _run_smoke_tests(self, cluster) -> TestResult:
        """Run smoke tests against a cluster."""
        tests = [
            self._test_health_endpoint(cluster),
            self._test_api_endpoint(cluster),
            self._test_database_connectivity(cluster),
        ]
        
        results = await asyncio.gather(*tests, return_exceptions=True)
        failures = [r for r in results if isinstance(r, Exception) or not r.passed]
        
        return TestResult(
            passed=len(failures) == 0,
            failures=[str(f) for f in failures]
        )
    
    async def _verify_deployment(self, version: str) -> VerificationResult:
        """Verify deployment is healthy in production."""
        
        # Check error rate
        error_rate = await self.metrics.query(
            'sum(rate(http_requests_total{status=~"5.."}[1m])) / '
            'sum(rate(http_requests_total[1m]))'
        )
        
        if error_rate > 0.01:  # > 1% error rate
            return VerificationResult(
                success=False,
                reason=f"Error rate too high: {error_rate:.2%}"
            )
        
        # Check latency
        p99_latency = await self.metrics.query(
            'histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[1m])) by (le))'
        )
        
        if p99_latency > 1.0:  # > 1 second
            return VerificationResult(
                success=False,
                reason=f"Latency too high: {p99_latency:.2f}s"
            )
        
        return VerificationResult(success=True, reason=None)

2.3 Blue-Green Pros and Cons#

Blue-green: when to use

Advantages

  • Instant rollback (just switch back)
  • Full testing before switch
  • Zero downtime
  • Clean separation of environments
  • Easy to understand

Disadvantages

  • Double infrastructure cost
  • Database must support both versions
  • Long-running connections may be disrupted
  • Not gradual (all-or-nothing switch)
  • Requires careful state management

Best for

  • Applications where instant rollback is critical
  • Teams with mature CI/CD pipelines
  • Services with clear version boundaries
  • When you can afford 2x infrastructure

Not ideal for

  • Services with lots of persistent connections
  • Applications with complex database migrations
  • Extremely high-traffic services (state issues)
  • Teams without automation
Quick check3 questions · +5 XP each

Rolling and blue-green deployments

  1. Q1/3 A rolling deployment of v1.1 is halfway done. Which requirement becomes non-negotiable at this point?

    Answer & explanation

    Answer: B

    Mid-rollout, half the instances run v1.0 and half run v1.1, so the database and APIs must support both at once. Double infrastructure is the cost of blue-green, not rolling, which only needs a small buffer.

  2. Q2/3 Ten minutes after switching traffic to green, errors spike. Why can blue-green roll back almost instantly?

    Answer & explanation

    Answer: C

    The old environment is left running untouched, so rollback is just another traffic switch with no deploy at all. Rolling back a rolling deployment, by contrast, means reversing the process instance by instance.

  3. Q3/3 Which service is the weakest fit for blue-green?

    Answer & explanation

    Answer: A

    The switch is all-or-nothing, so long-running connections on the old side get disrupted when traffic flips. The other three are exactly the cases the lesson lists as best fits for blue-green.

Chapter 3: Canary Deployment#

3.1 How Canary Works#

Named after "canary in a coal mine" — test with a small group first.

DiagramCanary deployment
%% title: Canary deployment
%% steps:
%% LB1 --> ST1, LB1 --> CA1: Stage 1: 1% of traffic goes to a single canary pod running v1.1
%% S1 --> S2: Watch it for 15 minutes before expanding
%% LB2 --> ST2, LB2 --> CA2: Stage 2: 10% of traffic to the canary
%% S2 --> S3, LB3 --> CA3: Stage 3: half the traffic, 5 pods of each version
%% S3 --> S4, LB4 --> ST4: Stage 4: v1.1 takes 100% and becomes the new stable version
flowchart TD
  subgraph S1["STAGE 1: CANARY (1% traffic)"]
    LB1@{ icon: "sd:load-balancer", label: "Load Balancer" } -->|"99%"| ST1@{ icon: "sd:servers", label: "STABLE v1.0<br/>10 pods" }
    LB1 -->|"1%"| CA1["CANARY v1.1<br/>1 pod"]:::core
  end
  subgraph S2["STAGE 2: EXPAND (10% traffic)"]
    LB2@{ icon: "sd:load-balancer", label: "Load Balancer" } -->|"90%"| ST2@{ icon: "sd:servers", label: "STABLE v1.0<br/>9 pods" }
    LB2 -->|"10%"| CA2["CANARY v1.1<br/>1 pod"]:::core
  end
  subgraph S3["STAGE 3: EXPAND (50% traffic)"]
    LB3@{ icon: "sd:load-balancer", label: "Load Balancer" } -->|"50%"| ST3@{ icon: "sd:servers", label: "STABLE v1.0<br/>5 pods" }
    LB3 -->|"50%"| CA3["CANARY v1.1<br/>5 pods"]:::core
  end
  subgraph S4["STAGE 4: COMPLETE (100% traffic)"]
    LB4@{ icon: "sd:load-balancer", label: "Load Balancer" } -->|"100%"| ST4@{ icon: "sd:servers", label: "STABLE v1.1<br/>10 pods" }
  end
  S1 -->|"Monitor for 15 minutes"| S2 --> S3 --> S4
  class LB1,LB2,LB3,LB4 edge
  class ST1,ST2,ST3 service
  class ST4 ok
  1. Stage 1: 1% of traffic goes to a single canary pod running v1.1
  2. Watch it for 15 minutes before expanding
  3. Stage 2: 10% of traffic to the canary
  4. Stage 3: half the traffic, 5 pods of each version
  5. Stage 4: v1.1 takes 100% and becomes the new stable version

The 1% stage needs weighted routing (a service mesh or a weighted load balancer). With plain round-robin, 1 canary pod among 10 stable pods would take 1 of 11 requests, about 9%.

Stage 1 — monitor for 15 minutes:

  • Compare error rate: canary vs stable
  • Compare latency: canary vs stable
  • Check business metrics
ExplainerCanary deployment with automated analysis

v1.1 gets 1%, then 10%, 50% and 100% of traffic, 15 minutes per stage, while its error rate and p99 latency are compared with stable (at most 1 point more errors, 10% more latency). Passing every stage promotes it; a failed analysis sets the canary weight to 0 and deletes the canary pods.

Your turn · checkpoint 2not answered

Estimate: how much does a bad canary hurt?

Assume the service takes 2,000 requests a second. The canary gets 1% of traffic for a 15-minute stage, and the new version fails 10% of its requests. How many requests fail before the stage ends, and how many would fail if the same release went to everyone for 15 minutes?

Think it through, or jot it down. Then compare.

One strong answer

  • Canary: 1% of 2,000 = 20 requests a second × 900 s = 18,000 requests, of which ~1,800 fail.
  • Everyone: 2,000 × 900 = 1.8 million requests, of which ~180,000 fail: 100 times more.
  • The catch: across the whole fleet the error rate rises by only 0.1 percentage points, which a global alert might miss. That's why the canary is compared side by side with stable.

3.2 Canary Implementation#

pythonCanaryStage · 263 lines
# deployment/canary.py

"""
Canary deployment with automated analysis.

Gradually rolls out new version while comparing
metrics against the stable version.
"""

from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime, timedelta, timezone
from enum import Enum
import asyncio
import logging

logger = logging.getLogger(__name__)


@dataclass
class CanaryStage:
    """A stage in the canary rollout."""
    name: str
    traffic_percentage: int
    duration_minutes: int
    auto_promote: bool = True  # Auto-promote if metrics pass


@dataclass
class CanaryConfig:
    """Configuration for canary deployment."""
    stages: List[CanaryStage] = field(default_factory=lambda: [
        CanaryStage("canary-1pct", 1, 15),
        CanaryStage("canary-10pct", 10, 15),
        CanaryStage("canary-50pct", 50, 15),
        CanaryStage("full-rollout", 100, 0),
    ])
    
    # Thresholds for automated analysis
    max_error_rate_increase: float = 0.01  # 1% absolute increase
    max_latency_increase_pct: float = 0.10  # 10% relative increase
    min_request_count: int = 100  # Minimum requests before analysis


@dataclass
class CanaryAnalysis:
    """Result of canary analysis."""
    passed: bool
    stable_error_rate: float
    canary_error_rate: float
    stable_p99_latency: float
    canary_p99_latency: float
    request_count: int
    reason: Optional[str] = None


class CanaryDeployer:
    """
    Orchestrates canary deployments with automated analysis.
    """
    
    def __init__(
        self,
        kubernetes_client,
        metrics_client,
        traffic_manager,
        config: CanaryConfig = None
    ):
        self.k8s = kubernetes_client
        self.metrics = metrics_client
        self.traffic = traffic_manager
        self.config = config or CanaryConfig()
    
    async def deploy(
        self,
        version: str,
        deployment_name: str,
        namespace: str = "default"
    ) -> CanaryResult:
        """
        Execute a canary deployment.
        """
        logger.info(f"Starting canary deployment of {version}")
        
        result = CanaryResult(
            version=version,
            started_at=datetime.now(timezone.utc),
            stages_completed=[]
        )
        
        try:
            # Deploy canary pods (initially with 0% traffic)
            await self._deploy_canary_pods(version, deployment_name, namespace)
            
            # Progress through stages
            for stage in self.config.stages:
                logger.info(f"Entering stage: {stage.name} ({stage.traffic_percentage}%)")
                
                # Shift traffic
                await self.traffic.set_canary_weight(
                    deployment_name,
                    stage.traffic_percentage
                )
                
                # Wait for stage duration
                if stage.duration_minutes > 0:
                    await asyncio.sleep(stage.duration_minutes * 60)
                    
                    # Analyze metrics
                    analysis = await self._analyze_canary(deployment_name)
                    
                    if not analysis.passed:
                        logger.warning(f"Canary failed at {stage.name}: {analysis.reason}")
                        await self._rollback(deployment_name, namespace)
                        result.failed = True
                        result.failure_reason = analysis.reason
                        result.failure_stage = stage.name
                        return result
                    
                    logger.info(f"Stage {stage.name} passed analysis")
                
                result.stages_completed.append(stage.name)
            
            # Promote canary to stable
            await self._promote_canary(deployment_name, namespace)
            
            result.completed_at = datetime.now(timezone.utc)
            result.success = True
            
            logger.info(f"Canary deployment complete: {version}")
            
            return result
            
        except Exception as e:
            logger.error(f"Canary deployment failed: {e}")
            await self._rollback(deployment_name, namespace)
            result.failed = True
            result.failure_reason = str(e)
            return result
    
    async def _analyze_canary(self, deployment_name: str) -> CanaryAnalysis:
        """
        Compare canary metrics against stable.
        """
        # Get error rates
        stable_errors = await self.metrics.query(f'''
            sum(rate(http_requests_total{{
                deployment="{deployment_name}",
                version="stable",
                status=~"5.."
            }}[5m])) /
            sum(rate(http_requests_total{{
                deployment="{deployment_name}",
                version="stable"
            }}[5m]))
        ''')
        
        canary_errors = await self.metrics.query(f'''
            sum(rate(http_requests_total{{
                deployment="{deployment_name}",
                version="canary",
                status=~"5.."
            }}[5m])) /
            sum(rate(http_requests_total{{
                deployment="{deployment_name}",
                version="canary"
            }}[5m]))
        ''')
        
        # Get latencies
        stable_latency = await self.metrics.query(f'''
            histogram_quantile(0.99,
                sum(rate(http_request_duration_seconds_bucket{{
                    deployment="{deployment_name}",
                    version="stable"
                }}[5m])) by (le)
            )
        ''')
        
        canary_latency = await self.metrics.query(f'''
            histogram_quantile(0.99,
                sum(rate(http_request_duration_seconds_bucket{{
                    deployment="{deployment_name}",
                    version="canary"
                }}[5m])) by (le)
            )
        ''')
        
        # Get request count
        request_count = await self.metrics.query(f'''
            sum(increase(http_requests_total{{
                deployment="{deployment_name}",
                version="canary"
            }}[5m]))
        ''')
        
        # Check thresholds
        analysis = CanaryAnalysis(
            passed=True,
            stable_error_rate=stable_errors,
            canary_error_rate=canary_errors,
            stable_p99_latency=stable_latency,
            canary_p99_latency=canary_latency,
            request_count=int(request_count)
        )
        
        # Not enough traffic to analyze: fail closed, never promote on no
        # evidence (alternatively, extend the stage and re-check)
        if analysis.request_count < self.config.min_request_count:
            logger.warning(f"Not enough requests for analysis: {analysis.request_count}")
            analysis.passed = False
            analysis.reason = (
                f"Insufficient canary traffic ({analysis.request_count} < "
                f"{self.config.min_request_count} requests)"
            )
            return analysis
        
        # Check error rate
        error_increase = canary_errors - stable_errors
        if error_increase > self.config.max_error_rate_increase:
            analysis.passed = False
            analysis.reason = (
                f"Error rate increased by {error_increase:.2%} "
                f"(threshold: {self.config.max_error_rate_increase:.2%})"
            )
            return analysis
        
        # Check latency
        if stable_latency > 0:
            latency_increase = (canary_latency - stable_latency) / stable_latency
            if latency_increase > self.config.max_latency_increase_pct:
                analysis.passed = False
                analysis.reason = (
                    f"Latency increased by {latency_increase:.2%} "
                    f"(threshold: {self.config.max_latency_increase_pct:.2%})"
                )
                return analysis
        
        return analysis
    
    async def _rollback(self, deployment_name: str, namespace: str):
        """Rollback canary deployment."""
        logger.info(f"Rolling back canary for {deployment_name}")
        
        # Remove canary traffic
        await self.traffic.set_canary_weight(deployment_name, 0)
        
        # Delete canary pods
        await self.k8s.delete_canary_deployment(deployment_name, namespace)
        
        logger.info("Rollback complete")
    
    async def _promote_canary(self, deployment_name: str, namespace: str):
        """Promote canary to stable."""
        logger.info(f"Promoting canary to stable for {deployment_name}")
        
        # Update stable deployment with canary version
        await self.k8s.promote_canary(deployment_name, namespace)
        
        # Remove canary traffic split
        await self.traffic.remove_canary_route(deployment_name)
        
        logger.info("Promotion complete")
WorksheetOne canary rollout, end to end (traffic and failure rate are assumed)
Value
Total traffic (assumed) 2,000 requests/s
Stage 1 canary share 1% → 20 requests/s
Requests in the 5-minute analysis window 20 × 300 = 6,000, well above the 100-request minimum
Requests the canary serves in its 15-minute stage 20 × 900 = 18,000
Same release to everyone for 15 minutes 2,000 × 900 = 1.8 million
A bug failing 10% of requests (assumed) ~1,800 failures on the canary vs ~180,000 fleet-wide
Fleet-wide error rate during stage 1 10% × 1% = +0.1 points
Time to reach 100% if every stage passes 3 stages × 15 minutes = 45 minutes

Conclusion: the 1% stage cuts the damage from a bad release 100-fold and still has plenty of traffic to judge. But the same bug barely moves the fleet's overall error rate, so the analysis must compare canary against stable rather than wait for a global alert.

3.3 Canary vs Blue-Green#

Canary vs blue-green comparison
Aspect Blue-Green Canary
Traffic switch All at once (100%) Gradual (1% → 100%)
Risk exposure All users at once Limited users first
Rollback speed Instant Fast (seconds)
Infrastructure cost 2x (the idle environment stays up for instant rollback) 1x + small overhead
Complexity Medium High
Comparison testing Before switch only Side-by-side continuous
Best for Quick full cutover Gradual risk reduction
When to use blue-green
  • Need instant rollback capability
  • Changes are well-tested
  • Want clean environment separation
  • Can afford 2x infrastructure
When to use canary
  • Want to minimize blast radius
  • Need production comparison metrics
  • High-traffic services (can't risk all users)
  • Automated analysis/rollback desired
Advanced: combine them
  • Blue-Green for environments
  • Canary for traffic within an environment
  • Best of both worlds
Your turn · checkpoint 3not answered

Design it: ship today, release next week

The new checkout flow is finished, but product wants it live for 10% of users next Tuesday, and you want to turn it off in seconds if it misbehaves. Each user should see the same checkout every time they visit. How would you ship it?

Think it through, or jot it down. Then compare.

One strong answer

  • Deploy the code today behind a flag that's off. Deployment and release become separate steps.
  • On Tuesday, enable it for 10% of users by hashing the flag key and user ID into a fixed bucket from 0 to 99, so each user always lands on the same side.
  • If it misbehaves, turn the flag off. No deploy is needed, and the old path is still in the code. Remove the flag once it reaches 100%.

Chapter 4: Feature Flags#

4.1 Feature Flags Concept#

Feature flags

Decouple DEPLOYMENT from RELEASE.

Flow Outcome
Traditional Deploy code → Feature is live Problem: Deploy is risky because features go live immediately
With Feature Flags Deploy code → Feature is OFF; Turn on flag → Feature is live Benefit: Deploy anytime, release when ready
Feature flag types
  1. RELEASE FLAGS — Control feature rollout
    • New checkout flow: OFF
    • Enable for 10% of users
    • Enable for beta users
    • Enable for everyone
  2. EXPERIMENT FLAGS — A/B testing
    • Button color: "blue" vs "green"
    • Pricing display: "monthly" vs "annual"
    • Measure conversion rates
  3. OPS FLAGS — Operational controls
    • Enable/disable expensive feature under load
    • Circuit breaker for external services
    • Kill switch for problematic code
  4. PERMISSION FLAGS — User-specific features
    • Premium features for paid users
    • Beta access for selected users
    • Admin features

4.2 Feature Flag Implementation#

pythonRolloutStrategy · 266 lines
# deployment/feature_flags.py

"""
Feature flag system for controlled rollouts.

Supports:
- Boolean flags (on/off)
- Percentage rollouts
- User targeting
- Environment targeting
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Callable
from enum import Enum
import hashlib
import logging

logger = logging.getLogger(__name__)


class RolloutStrategy(Enum):
    ALL = "all"           # Flag applies to everyone
    NONE = "none"         # Flag applies to no one
    PERCENTAGE = "percentage"  # Random percentage of users
    USER_LIST = "user_list"    # Specific users
    ATTRIBUTE = "attribute"     # Based on user attributes


@dataclass
class FeatureFlag:
    """A feature flag definition."""
    key: str
    name: str
    description: str
    
    # Targeting
    strategy: RolloutStrategy
    percentage: int = 0  # For PERCENTAGE strategy
    user_list: List[str] = field(default_factory=list)  # For USER_LIST
    attribute_rules: List[Dict] = field(default_factory=list)  # For ATTRIBUTE
    
    # Metadata
    owner: str = ""
    created_at: datetime = None
    expires_at: Optional[datetime] = None
    
    # Default value
    default_value: bool = False
    
    # Variants (for A/B tests)
    variants: Dict[str, Any] = field(default_factory=dict)


@dataclass
class EvaluationContext:
    """Context for evaluating a feature flag."""
    user_id: Optional[str] = None
    tenant_id: Optional[str] = None
    environment: str = "production"
    attributes: Dict[str, Any] = field(default_factory=dict)


class FeatureFlagService:
    """
    Feature flag evaluation service.
    """
    
    def __init__(self, flag_store, metrics_client):
        self.store = flag_store
        self.metrics = metrics_client
        self._cache = {}
    
    def is_enabled(
        self,
        flag_key: str,
        context: EvaluationContext = None
    ) -> bool:
        """
        Check if a feature flag is enabled.
        
        Usage:
            if feature_flags.is_enabled("new_checkout", context):
                return new_checkout_flow()
            else:
                return old_checkout_flow()
        """
        context = context or EvaluationContext()
        
        try:
            flag = self._get_flag(flag_key)
            
            if flag is None:
                logger.warning(f"Unknown flag: {flag_key}")
                return False
            
            # Check if expired
            if flag.expires_at and datetime.now(timezone.utc) > flag.expires_at:
                return flag.default_value
            
            # Evaluate based on strategy
            result = self._evaluate(flag, context)
            
            # Record metric
            self.metrics.increment(
                "feature_flag_evaluation_total",
                labels={
                    "flag": flag_key,
                    "result": str(result),
                    "strategy": flag.strategy.value
                }
            )
            
            return result
            
        except Exception as e:
            logger.error(f"Error evaluating flag {flag_key}: {e}")
            return False
    
    def get_variant(
        self,
        flag_key: str,
        context: EvaluationContext = None
    ) -> Optional[str]:
        """
        Get variant for A/B test flags.
        
        Usage:
            variant = feature_flags.get_variant("button_color", context)
            # Returns "blue" or "green" based on user bucketing
        """
        context = context or EvaluationContext()
        flag = self._get_flag(flag_key)
        
        if flag is None or not flag.variants:
            return None
        
        # Consistent hashing to assign user to variant
        bucket = self._get_bucket(flag_key, context.user_id)
        
        # Distribute buckets across variants
        cumulative = 0
        for variant_name, percentage in flag.variants.items():
            cumulative += percentage
            if bucket < cumulative:
                return variant_name
        
        return list(flag.variants.keys())[0]  # Default to first variant
    
    def _evaluate(self, flag: FeatureFlag, context: EvaluationContext) -> bool:
        """Evaluate flag based on its strategy."""
        
        if flag.strategy == RolloutStrategy.ALL:
            return True
        
        if flag.strategy == RolloutStrategy.NONE:
            return False
        
        if flag.strategy == RolloutStrategy.PERCENTAGE:
            return self._evaluate_percentage(flag, context)
        
        if flag.strategy == RolloutStrategy.USER_LIST:
            return context.user_id in flag.user_list
        
        if flag.strategy == RolloutStrategy.ATTRIBUTE:
            return self._evaluate_attributes(flag, context)
        
        return flag.default_value
    
    def _evaluate_percentage(
        self,
        flag: FeatureFlag,
        context: EvaluationContext
    ) -> bool:
        """
        Percentage-based evaluation with consistent hashing.
        
        Same user always gets same result for same flag.
        """
        bucket = self._get_bucket(flag.key, context.user_id)
        return bucket < flag.percentage
    
    def _get_bucket(self, flag_key: str, user_id: str) -> int:
        """
        Get consistent bucket (0-99) for user and flag.
        
        Same user + flag = same bucket, always.
        """
        if not user_id:
            # Random for anonymous users (NOT sticky: pass a session or device ID for a stable assignment)
            import random
            return random.randint(0, 99)
        
        # Hash user_id + flag_key for consistent bucketing
        hash_input = f"{flag_key}:{user_id}"
        hash_value = hashlib.md5(hash_input.encode()).hexdigest()
        return int(hash_value[:8], 16) % 100
    
    def _evaluate_attributes(
        self,
        flag: FeatureFlag,
        context: EvaluationContext
    ) -> bool:
        """Evaluate attribute-based rules."""
        for rule in flag.attribute_rules:
            attribute = rule.get("attribute")
            operator = rule.get("operator")
            value = rule.get("value")
            
            user_value = context.attributes.get(attribute)
            
            if operator == "equals" and user_value == value:
                return True
            if operator == "in" and user_value in value:
                return True
            if operator == "greater_than" and user_value > value:
                return True
        
        return flag.default_value
    
    def _get_flag(self, flag_key: str) -> Optional[FeatureFlag]:
        """Get flag from store with caching.

        Note: this cache has no TTL, so flag changes (including kill switches)
        would not propagate until restart. Real systems expire or refresh entries.
        """
        if flag_key in self._cache:
            return self._cache[flag_key]
        
        flag = self.store.get(flag_key)
        if flag:
            self._cache[flag_key] = flag
        
        return flag


# =============================================================================
# USAGE EXAMPLES
# =============================================================================

# Initialize
feature_flags = FeatureFlagService(flag_store, metrics)

# Simple boolean check
if feature_flags.is_enabled("new_checkout"):
    process_with_new_checkout()
else:
    process_with_old_checkout()

# With user context
context = EvaluationContext(
    user_id="user-123",
    tenant_id="tenant-456",
    attributes={
        "plan": "enterprise",
        "country": "US"
    }
)

if feature_flags.is_enabled("advanced_analytics", context):
    show_advanced_analytics()

# A/B test
button_color = feature_flags.get_variant("button_color_test", context)
render_button(color=button_color)

4.3 Feature Flag Best Practices#

Feature flag best practices
1. Have a lifecycle

Flags should be temporary!

Lifecycle:

  1. Create flag (with owner and expiration)
  2. Roll out gradually
  3. Enable for 100%
  4. REMOVE FLAG from code
  5. Archive flag

Stale flags are tech debt.

2. Keep flags simple

Flags should gate features, not business logic.

  • GOOD: feature_flags.is_enabled("new_search")
  • BAD: feature_flags.get_value("search_algorithm_config")

Complex configuration should be in config files, not flags.

3. Test both paths

Every flag creates a code branch. Test both.

pythontest_feature_enabled · 7 lines
def test_feature_enabled():
    with flag_enabled("new_checkout"):
        assert checkout() == expected_new

def test_feature_disabled():
    with flag_disabled("new_checkout"):
        assert checkout() == expected_old
4. Have kill switches

Some flags should always exist for operational control.

Examples:

  • disable_expensive_queries
  • enable_read_only_mode
  • disable_external_integrations
  • enable_maintenance_mode

These are not temporary — they're operational controls.

5. Monitor flag usage

Track:

  • How often each flag is evaluated
  • Which flags are always true/false (candidates for removal)
  • Which flags are never evaluated (dead code)
  • Errors in flag evaluation
Quick check3 questions · +5 XP each

Canary releases and feature flags

  1. Q1/3 A canary at 1% has served 5,000 requests. Error rate is 0.4% vs 0.3% on stable, and canary p99 is 25% higher than stable. With the lesson's thresholds (1% absolute error increase, 10% relative latency increase), what happens?

    Answer & explanation

    Answer: D

    Analysis checks error rate and latency separately; the error increase passes, but a 25% relative p99 increase breaks the 10% limit. 5,000 requests is well above the 100-request minimum, so there is enough data to act on.

  2. Q2/3 You roll out new_checkout to 10% of users. A user refreshes ten times. How does the lesson's flag service make sure they see the same experience every time?

    Answer & explanation

    Answer: C

    Hashing flag_key:user_id gives the same bucket for the same user every time, so the experience is stable with no stored state. A per-request random draw would flip users between old and new checkout on every refresh.

  3. Q3/3 Which flag should stay in the code permanently rather than being removed after rollout?

    Answer & explanation

    Answer: A

    Kill switches are operational controls and are meant to exist permanently. Release and experiment flags that are fully rolled out or finished are stale tech debt and should be removed.

Part II · Stage 2 of 4 · 10 min

Part II: Advanced Topics#

Chapter 5: Database Migrations#

5.1 The Database Migration Problem#

The database migration challenge
Code deployments are (relatively) easy
  • Deploy new code
  • If broken, rollback to old code
  • Done
Database changes are HARD
  • Can't just "rollback" a migration
  • Schema changes affect running code
  • Data migrations can take hours
  • Both old and new code must work with DB
  • Mistakes can cause data loss
The key insight

During deployment, BOTH versions of code are running. Your database must support BOTH versions.

Your turn · checkpoint 4not answered

What breaks if you just rename the column?

You need users.name to become users.full_name. The plan: run ALTER TABLE users RENAME COLUMN name TO full_name, then do a rolling deploy of code that uses full_name. What breaks during the rollout, and what order of steps would avoid it?

Think it through, or jot it down. Then compare.

One strong answer

  • During the rollout both versions are live. Right after the rename, every old instance still queries name and fails; rename later and the new instances fail instead.
  • Avoid it by never breaking either version: add full_name and write to both; backfill it and switch reads to it; drop name only once no running code uses it.
  • Each step works with both old and new code, so each can be rolled back on its own.

5.2 Expand-Contract Pattern#

Expand-contract (parallel change) pattern

The safe way to make breaking schema changes.

Example: Rename column name to full_name

WRONG: Direct rename
  1. Deploy migration: ALTER TABLE users RENAME COLUMN name TO full_name;
  2. Deploy code that uses full_name

Problem: During deployment, old code (using name) and new code (using full_name) are both running. One of them will break.

RIGHT: Expand-Contract (3-phase migration)

PHASE 1: EXPAND

  • Add new column
  • Write to both columns
  • Read from old column

Migration:

sql1 line
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

Code:

sql4 lines
# Write to both
UPDATE users SET name = $1, full_name = $1 WHERE id = $2
# Read from old
SELECT name FROM users WHERE id = $1

Run the migration first (the column must exist before any code writes to it), then deploy the code. Both old and new code work.

PHASE 2: MIGRATE

  • Backfill data
  • Switch reads to new column
  • Continue writing to both

Migration:

sql1 line
UPDATE users SET full_name = name WHERE full_name IS NULL;

Code:

sql4 lines
# Write to both (for safety)
UPDATE users SET name = $1, full_name = $1 WHERE id = $2
# Read from new
SELECT full_name FROM users WHERE id = $1

Run the backfill first, then deploy the code that reads the new column (otherwise un-backfilled rows read as NULL).

PHASE 3: CONTRACT

  • Stop writing to old column
  • Remove old column

Code:

sql4 lines
# Write to new only
UPDATE users SET full_name = $1 WHERE id = $2
# Read from new
SELECT full_name FROM users WHERE id = $1

Migration (after all old code is gone):

sql1 line
ALTER TABLE users DROP COLUMN name;
Timeline
Original Expand Migrate Contract
name only name + full_name (write both) name + full_name (read new) full_name only

5.3 Migration Implementation#

Note: the samples call fetch_one, as in the databases library (or a thin wrapper with that method). With asyncpg the equivalent is fetchrow, which returns a Record.

pythonMigrationPhase · 236 lines
# deployment/migrations.py

"""
Safe database migration patterns.

Implements expand-contract for breaking changes.
"""

from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
import logging
from datetime import datetime, timezone

logger = logging.getLogger(__name__)


class MigrationPhase(Enum):
    EXPAND = "expand"
    MIGRATE = "migrate"
    CONTRACT = "contract"


@dataclass
class Migration:
    """A database migration."""
    version: str
    name: str
    phase: MigrationPhase
    
    # SQL to run
    up_sql: str
    down_sql: str
    
    # Validation
    pre_check_sql: Optional[str] = None  # Must pass before running
    post_check_sql: Optional[str] = None  # Must pass after running
    
    # Safety
    requires_lock: bool = False  # Requires table lock
    estimated_duration: str = ""  # e.g., "5 minutes", "2 hours"
    affects_writes: bool = False  # Will block writes


class SafeMigrationRunner:
    """
    Runs migrations safely with validation and rollback support.
    """
    
    def __init__(self, db_connection, lock_manager):
        self.db = db_connection
        self.locks = lock_manager
    
    async def run_migration(
        self,
        migration: Migration,
        dry_run: bool = False
    ) -> MigrationResult:
        """
        Run a migration with safety checks.
        """
        logger.info(f"Starting migration: {migration.name}")
        
        result = MigrationResult(
            migration=migration,
            started_at=datetime.now(timezone.utc)
        )
        
        try:
            # Pre-check
            if migration.pre_check_sql:
                check_result = await self.db.fetch_one(migration.pre_check_sql)
                if not check_result or not check_result[0]:
                    raise MigrationError("Pre-check failed")
            
            # Acquire lock if needed
            if migration.requires_lock:
                await self.locks.acquire("migration_lock")
            
            # Run migration
            if not dry_run:
                await self.db.execute(migration.up_sql)
            else:
                logger.info(f"DRY RUN: Would execute: {migration.up_sql}")
            
            # Post-check
            if migration.post_check_sql:
                check_result = await self.db.fetch_one(migration.post_check_sql)
                if not check_result or not check_result[0]:
                    raise MigrationError("Post-check failed, rolling back")
            
            # Record success
            await self._record_migration(migration)
            
            result.success = True
            result.completed_at = datetime.now(timezone.utc)
            
            logger.info(f"Migration complete: {migration.name}")
            
            return result
            
        except Exception as e:
            logger.error(f"Migration failed: {e}")
            
            # Attempt rollback
            if not dry_run:
                try:
                    await self.db.execute(migration.down_sql)
                    logger.info("Rollback successful")
                except Exception as rollback_error:
                    logger.error(f"Rollback failed: {rollback_error}")
            
            result.success = False
            result.error = str(e)
            
            return result
            
        finally:
            if migration.requires_lock:
                await self.locks.release("migration_lock")


# =============================================================================
# EXAMPLE: RENAME COLUMN MIGRATION SET
# =============================================================================

RENAME_COLUMN_MIGRATIONS = [
    # Phase 1: Expand - Add new column
    Migration(
        version="2024_01_15_001",
        name="add_full_name_column",
        phase=MigrationPhase.EXPAND,
        up_sql="""
            ALTER TABLE users 
            ADD COLUMN full_name VARCHAR(255);
            
            -- Trigger to keep columns in sync during transition
            CREATE OR REPLACE FUNCTION sync_name_columns()
            RETURNS TRIGGER AS $$
            BEGIN
                IF TG_OP = 'INSERT' THEN
                    -- Old code inserts only name; new code may set both
                    NEW.full_name := COALESCE(NEW.full_name, NEW.name);
                    NEW.name := COALESCE(NEW.name, NEW.full_name);
                ELSIF NEW.name IS DISTINCT FROM OLD.name THEN
                    NEW.full_name := NEW.name;
                ELSIF NEW.full_name IS DISTINCT FROM OLD.full_name THEN
                    NEW.name := NEW.full_name;
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql;
            
            CREATE TRIGGER sync_names
            BEFORE INSERT OR UPDATE ON users
            FOR EACH ROW EXECUTE FUNCTION sync_name_columns();
        """,
        down_sql="""
            DROP TRIGGER IF EXISTS sync_names ON users;
            DROP FUNCTION IF EXISTS sync_name_columns();
            ALTER TABLE users DROP COLUMN IF EXISTS full_name;
        """,
        post_check_sql="""
            SELECT EXISTS (
                SELECT 1 FROM information_schema.columns 
                WHERE table_name = 'users' AND column_name = 'full_name'
            );
        """
    ),
    
    # Phase 2: Migrate - Backfill data
    Migration(
        version="2024_01_16_001",
        name="backfill_full_name",
        phase=MigrationPhase.MIGRATE,
        up_sql="""
            -- Backfill in batches to avoid locking
            DO $$
            DECLARE
                batch_size INT := 10000;
                updated INT;
            BEGIN
                LOOP
                    UPDATE users 
                    SET full_name = name 
                    WHERE full_name IS NULL 
                    AND id IN (
                        SELECT id FROM users 
                        WHERE full_name IS NULL 
                        LIMIT batch_size
                    );
                    
                    GET DIAGNOSTICS updated = ROW_COUNT;
                    EXIT WHEN updated = 0;
                    
                    -- Commit each batch so its row locks are released
                    -- (PostgreSQL 11+; the DO block must not run inside
                    -- an explicit transaction, or it is all one transaction)
                    COMMIT;
                    
                    -- Small pause between batches
                    PERFORM pg_sleep(0.1);
                END LOOP;
            END $$;
        """,
        down_sql="-- No rollback needed for backfill",
        post_check_sql="""
            SELECT COUNT(*) = 0 
            FROM users 
            WHERE full_name IS NULL AND name IS NOT NULL;
        """,
        estimated_duration="30 minutes for 1M rows"
    ),
    
    # Phase 3: Contract - Remove old column (after code fully migrated)
    Migration(
        version="2024_01_20_001",
        name="remove_name_column",
        phase=MigrationPhase.CONTRACT,
        up_sql="""
            DROP TRIGGER IF EXISTS sync_names ON users;
            DROP FUNCTION IF EXISTS sync_name_columns();
            ALTER TABLE users DROP COLUMN name;
        """,
        down_sql="""
            ALTER TABLE users ADD COLUMN name VARCHAR(255);
            UPDATE users SET name = full_name;
        """,
        pre_check_sql="""
            -- Ensure no code is reading from 'name' column
            -- This should be checked via application logs/metrics
            SELECT true;
        """,
        requires_lock=True
    ),
]

Chapter 6: Rollback Strategies#

6.1 Types of Rollback#

Rollback strategies
Type Speed Risk
1. Code rollback Fast (seconds to minutes) Low if code is backward compatible
2. Feature rollback Instant (seconds) Very low
3. Config rollback Minutes Low to medium
4. Data rollback Slow (minutes to hours) High (potential data loss)
1. Code rollback

Roll back to previous code version.

Methods:

  • kubectl rollout undo deployment/myapp
  • Deploy previous version tag
  • Blue-green: switch back to previous environment
  • Revert git commit, redeploy
2. Feature rollback

Turn off feature flag without code change.

Methods:

  • Flip flag in feature flag service
  • No deployment needed
  • Instant effect
3. Config rollback

Revert configuration change.

Methods:

  • Update ConfigMap/environment variables
  • Rolling restart to pick up new config
  • Some systems support hot reload
4. Data rollback

Revert data changes. THE HARDEST TYPE.

Methods:

  • Restore from backup (slow, nuclear option)
  • Run compensating transactions
  • Point-in-time recovery
  • Manual data fixes

BEST APPROACH: Avoid needing data rollback

  • Test migrations thoroughly
  • Use expand-contract pattern
  • Make changes reversible
  • Take backups before risky changes

6.2 Automated Rollback#

pythonRollbackConfig · 145 lines
# deployment/auto_rollback.py

"""
Automated rollback based on metrics.

If deployment causes problems, automatically roll back.
"""

from dataclasses import dataclass
from typing import Optional
import asyncio
import logging
from datetime import datetime, timezone

logger = logging.getLogger(__name__)


@dataclass
class RollbackConfig:
    """Configuration for automated rollback."""
    # Metric thresholds
    max_error_rate: float = 0.05  # 5%
    max_latency_p99_ms: int = 1000  # 1 second
    min_success_rate: float = 0.95  # 95%
    
    # Timing
    observation_window_seconds: int = 300  # 5 minutes
    check_interval_seconds: int = 30
    
    # Behavior
    auto_rollback_enabled: bool = True
    require_confirmation: bool = False  # For critical services


class AutomatedRollbackMonitor:
    """
    Monitors deployments and triggers rollback if metrics degrade.
    """
    
    def __init__(
        self,
        metrics_client,
        deployer,
        alerter,
        config: RollbackConfig = None
    ):
        self.metrics = metrics_client
        self.deployer = deployer
        self.alerter = alerter
        self.config = config or RollbackConfig()
    
    async def monitor_deployment(
        self,
        deployment_id: str,
        baseline_metrics: dict
    ):
        """
        Monitor a deployment and rollback if needed.
        """
        logger.info(f"Starting rollback monitor for deployment {deployment_id}")
        
        start_time = datetime.now(timezone.utc)
        end_time = start_time + timedelta(seconds=self.config.observation_window_seconds)
        
        while datetime.now(timezone.utc) < end_time:
            await asyncio.sleep(self.config.check_interval_seconds)
            
            # Check metrics
            current_metrics = await self._get_current_metrics()
            
            # Compare to baseline and thresholds
            should_rollback, reason = self._should_rollback(
                baseline_metrics,
                current_metrics
            )
            
            if should_rollback:
                logger.warning(f"Rollback triggered: {reason}")
                
                if self.config.require_confirmation:
                    # Alert and wait for confirmation
                    await self.alerter.send_critical(
                        f"Deployment {deployment_id} may need rollback: {reason}. "
                        f"Reply 'ROLLBACK' to confirm."
                    )
                    # In a real system, this would wait for response
                elif self.config.auto_rollback_enabled:
                    await self._execute_rollback(deployment_id, reason)
                    return RollbackResult(
                        rolled_back=True,
                        reason=reason,
                        metrics=current_metrics
                    )
        
        logger.info(f"Deployment {deployment_id} passed monitoring period")
        return RollbackResult(rolled_back=False, metrics=current_metrics)
    
    def _should_rollback(
        self,
        baseline: dict,
        current: dict
    ) -> tuple[bool, Optional[str]]:
        """
        Determine if rollback is needed.
        """
        # Check absolute thresholds
        if current['error_rate'] > self.config.max_error_rate:
            return True, f"Error rate {current['error_rate']:.2%} exceeds threshold"
        
        if current['latency_p99_ms'] > self.config.max_latency_p99_ms:
            return True, f"Latency {current['latency_p99_ms']}ms exceeds threshold"
        
        if current['success_rate'] < self.config.min_success_rate:
            return True, f"Success rate {current['success_rate']:.2%} below threshold"
        
        # Check relative degradation vs baseline
        if baseline:
            error_increase = current['error_rate'] - baseline['error_rate']
            if error_increase > 0.02:  # 2% absolute increase
                return True, f"Error rate increased by {error_increase:.2%}"
            
            latency_increase_pct = (
                (current['latency_p99_ms'] - baseline['latency_p99_ms']) /
                baseline['latency_p99_ms']
            ) if baseline['latency_p99_ms'] > 0 else 0
            
            if latency_increase_pct > 0.5:  # 50% increase
                return True, f"Latency increased by {latency_increase_pct:.0%}"
        
        return False, None
    
    async def _execute_rollback(self, deployment_id: str, reason: str):
        """Execute the rollback."""
        logger.warning(f"Executing rollback for {deployment_id}")
        
        # Send alert
        await self.alerter.send_critical(
            f"AUTO-ROLLBACK executed for deployment {deployment_id}. "
            f"Reason: {reason}"
        )
        
        # Execute rollback
        await self.deployer.rollback(deployment_id)
        
        logger.info(f"Rollback complete for {deployment_id}")
Quick check3 questions · +5 XP each

Migrations and rollback

  1. Q1/3 Why does a direct ALTER TABLE users RENAME COLUMN name TO full_name break a zero-downtime deploy?

    Answer & explanation

    Answer: B

    During a deploy both versions are live, so the database has to support both; after a rename, whichever version uses the other name fails. The data survives a rename; the break comes from the two code versions disagreeing on the schema.

  2. Q2/3 In the expand-contract rename, when is it safe to run ALTER TABLE users DROP COLUMN name?

    Answer & explanation

    Answer: D

    The contract phase drops the old column only after all old code is gone. In the migrate phase, code still writes both columns and older instances may still read name, so dropping it then breaks them.

  3. Q3/3 A new recommendation widget, shipped behind a feature flag in today's release, is throwing errors. What is the fastest, lowest-risk way to stop the impact?

    Answer & explanation

    Answer: A

    Feature rollback takes effect in seconds with no deploy, and it leaves the rest of the release in place. A code rollback also works, but it takes longer and reverts every other change that shipped with it.

Part III · Stage 3 of 4 · 3 min

Part III: Real-World Application#

Chapter 7: Case Studies#

7.1 How Amazon Deploys#

Amazon's deployment practices
Scale
  • Thousands of deployments per day
  • Thousands of services
  • Millions of servers
  • Zero-downtime expectation
Key practices
  1. ONE-BOX DEPLOYMENT
    • Deploy to single instance first
    • Run tests and monitor
    • If healthy, proceed to wider rollout
    • If not, automatic rollback
  2. WAVE DEPLOYMENTS
    • Deploy in waves of growing size (the exact sizes are each team's choice)
    • Bake time between waves
    • Automated health checks between waves
    • Automatic rollback if metrics degrade
  3. REGION-BY-REGION
    • Deploy to one region first
    • Verify in production
    • Then roll out to other regions
    • Can quickly isolate issues
  4. FEATURE FLAGS
    • Deploy code dark
    • Enable features gradually
    • A/B testing for new features
    • Kill switches for risky features
  5. APOLLO + DYNAMIC CONFIG
    • Apollo is Amazon's internal deployment engine (pipelines, version tracking, rollback)
    • Config is separated from code and changed without a deploy
    • Config changes are validated and rolled out gradually (the approach AWS later offered as AppConfig)
    • Versioned config with rollback
Lessons
  • Small changes are safer than big changes
  • Automate everything
  • Monitor everything
  • Roll back fast
  • Deploy often to reduce batch size

7.2 How Google Does It#

Google's deployment approach

Philosophy: make rollbacks easy and deployments boring.

Key systems
  1. BORG/KUBERNETES
    • Declarative deployment
    • Automated rollout/rollback
    • Health checks built-in
    • Self-healing
  2. STAGED ROLLOUTS
    • Canary first
    • Automated analysis
    • Progressive percentage increase
    • Cross-cluster, cross-region
  3. RELEASE TRAINS
    • Regular release cadence
    • Features either make the train or wait
    • Predictable release schedule
    • Reduces urgency and risk
  4. BINARY VS CONFIG
    • Binary releases: on a regular cadence (varies by team, from hourly builds to scheduled releases)
    • Config changes: anytime, through their own rollout
    • Many behaviour changes ship as config
    • Reduces code deployment risk
  5. CANARYING EVERYTHING
    • Code changes: Canary
    • Config changes: Canary
    • Capacity changes: Canary
    • Everything that can break should go through a canary
SRE integration
  • Release engineers build the release tooling (such as Rapid); SREs use it and build rollout automation for complex deployments
  • Error budget gates deployments
  • If SLO violated, deployment paused
  • Reliability is a gating criterion

Chapter 8: Common Mistakes#

Deployment anti-patterns
Mistake 1: Big Bang Deployments

Wrong:

  • Save up changes for months
  • Deploy everything at once
  • Hope for the best

Problems:

  • Huge blast radius if something breaks
  • Hard to identify which change caused issues
  • Rollback affects all changes

Right:

  • Deploy frequently (daily or more)
  • Small, incremental changes
  • Easy to identify and rollback specific changes
Mistake 2: No Rollback Plan

Wrong: "We'll figure out rollback if we need it"

Problems:

  • Panic when things break
  • Unclear process under pressure
  • Longer outages

Right:

  • Test rollback BEFORE you need it
  • Document rollback procedure
  • Automate if possible
  • Practice regularly
Mistake 3: Database Changes with Code

Wrong:

  • Code and schema change in same deployment
  • Migration runs during deployment
  • If either fails, both rollback needed

Problems:

  • Complex rollback
  • Data migration might not be reversible
  • Extended downtime

Right:

  • Separate database changes from code changes
  • Use expand-contract pattern
  • Database changes first, then code
  • Each can roll back independently
Mistake 4: Deploy on Friday

Wrong: "Let's ship this before the weekend!"

Problems:

  • Reduced staffing if issues
  • Tired people making decisions
  • Customer impact over weekend

Right:

  • Deploy early in the week
  • Deploy early in the day
  • Have full team available for monitoring
  • Friday deployments only for emergencies
Mistake 5: No Monitoring During Deploy

Wrong:

  • Deploy and walk away
  • Assume success
  • Find out about problems from customers

Right:

  • Watch dashboards during deploy
  • Have alerts set for deployment metrics
  • Bake time with active monitoring
  • Only declare success after observation period
Quick check3 questions · +5 XP each

Deployment practice at scale

  1. Q1/3 A change passes every pre-production test. What does Amazon's one-box-then-waves approach give you that a single fleet-wide push does not?

    Answer & explanation

    Answer: C

    Bake time and health checks at each wave limit the blast radius: problems show up on one box or a small percentage first. Waves are slower than one push, not faster, and they depend on automated checks rather than replacing them.

  2. Q2/3 Your service has burned through its error budget for the month. Under the SRE practice described for Google, what happens to the next code release?

    Answer & explanation

    Answer: B

    The error budget gates deployments: if the SLO is violated, deployment is paused, which makes reliability a gating criterion. A longer canary still spends budget you no longer have.

  3. Q3/3 Two minutes after a deploy, every dashboard is green. When should you declare it a success?

    Answer & explanation

    Answer: A

    Some failures only show up under real load over time, so success is declared only after the observation period. Waiting for customer tickets means finding out from customers, which is the anti-pattern itself.

Part IV · Stage 4 of 4 · 5 min

Part IV: Interview Preparation#

Chapter 9: Interview Tips#

9.1 Deployment Discussion Framework#

Discussing deployments in interviews

When asked "How would you deploy changes to this system?":

  1. ASSESS RISK — "First, I'd assess the risk of this change. Is it a small bug fix or a major feature? Does it touch critical paths like payments? Does it require database changes?"
  2. CHOOSE STRATEGY — "Based on risk, I'd choose a deployment strategy:"
    • Low risk: Rolling deployment with monitoring
    • Medium risk: Canary with automated analysis
    • High risk: Blue-green with feature flag
  3. EXPLAIN SAFETY MECHANISMS — "I'd ensure safety through:"
    • Health checks before declaring success
    • Metrics comparison (canary vs stable)
    • Automated rollback if thresholds exceeded
  4. ADDRESS DATABASE CHANGES — "For database changes, I'd use expand-contract:"
    • Phase 1 (expand): Add new column/table; write to both
    • Phase 2 (migrate): Backfill data, then switch reads to the new schema
    • Phase 3 (contract): Remove old schema once no code uses it
  5. DISCUSS ROLLBACK — "If something goes wrong:"
    • Blue-green: Instant switch back
    • Canary: Remove canary traffic
    • Feature flag: Turn off flag
    • Code: Rollback to previous version
❯ Say this

9.2 Key Phrases#

Deployment key phrases
On strategy selection

"I match deployment strategy to risk. For a new payment feature, I'd use canary with aggressive monitoring. For a UI tweak, a standard rolling deployment is fine."

On canary

"Canary lets me compare the new version against the stable version in production with real traffic. If error rate increases or latency degrades, I roll back before most users are affected."

On feature flags

"Feature flags decouple deployment from release. I can deploy code anytime, then enable the feature gradually. If something's wrong, turning off the flag is instant — no deployment needed."

On database migrations

"I never make breaking schema changes directly. I use expand-contract: add the new structure, migrate data, switch the code, then remove the old structure. This way, both old and new code work at every step."

On rollback

"The first thing I consider with any change is: how do I undo this? If I can't answer that, I need to rethink the approach. Rollback should be fast and safe."

Chapter 10: Practice Problems#

Practice · problem 1

Problem 1: E-commerce Checkout Deployment#

Scenario: You're deploying a new checkout flow. How would you do it safely?

Questions:

  1. What deployment strategy would you use?
  2. What metrics would you monitor?
  3. What's your rollback plan?
Click for approach
  1. Strategy: Canary with feature flag

    • Deploy code with feature flag OFF
    • Enable for 1% of users, monitor
    • Gradually increase to 100%
  2. Metrics:

    • Checkout completion rate
    • Error rate
    • Payment success rate
    • Latency p99
    • Cart abandonment rate
  3. Rollback:

    • Turn off feature flag (instant)
    • If persistent issue, roll back code deployment

Practice · problem 2

Problem 2: Database Column Rename#

Scenario: You need to rename a column from email to primary_email. The table has 10 million rows.

Questions:

  1. What's the safest approach?
  2. What are the migration phases?
  3. How do you handle the transition period?
Click for approach
  1. Approach: Expand-Contract

    • Never directly rename a column
    • Three-phase migration
  2. Phases:

    • Expand: Add primary_email column
    • Migrate: Backfill data, switch code to use new column
    • Contract: Remove email column
  3. Transition:

    • Code writes to BOTH columns
    • Code reads from new column (with fallback to old)
    • Once all code updated, remove old column

Chapter 11: Sample Interview Dialogue#

Transcript
Interviewer

"You have a new feature that changes how we calculate shipping costs. How would you deploy it?"

You

"Let me think through this systematically. First, let me ask a few questions:

  • How critical is shipping cost accuracy? Are mistakes costly?
  • Is there a database component to this change?
  • How much traffic does this code path see?"
Transcript
Interviewer

"It's a high-volume checkout flow. Mistakes in shipping costs directly impact revenue. There's a new table for shipping zones."

You

"Given the high risk, I'd use a careful approach:

Phase 1: Database changes first"

  1. Create new shipping_zones table
  2. Populate with data
  3. No code changes yet
  4. Verify data is correct

"Phase 2: Deploy code behind feature flag"

  1. Deploy new calculation code with flag OFF
  2. Both old and new code paths exist
  3. No user impact yet

"Phase 3: Shadow testing"

  1. Run new calculation alongside old
  2. Log both results, compare
  3. Look for discrepancies
  4. Fix any issues found

"Phase 4: Gradual rollout"

  1. Enable for internal users first
  2. Then 1% of customers
  3. Monitor:
    • Shipping cost accuracy
    • Checkout completion rate
    • Customer complaints
    • Revenue metrics
  4. Gradually increase to 100%
Transcript
Interviewer

"What if you find a bug at 10% rollout?"

You

"I'd turn off the feature flag immediately. Users would get the old calculation. Since the old code is still there, this is instant.

Then I'd analyze the bug, fix it, and start the rollout again from shadow testing.

The beauty of feature flags is that rollback is just a configuration change — no deployment needed."

Interviewer

"Good. How would you handle a bug in the database changes?"

You

"That's trickier. Database changes are harder to roll back.

For the shipping_zones table:

  • It's additive (new table), so it doesn't break existing code
  • If the data is wrong, I can update it without schema changes
  • If the schema itself is wrong, I'd create a new table with the correct schema, migrate data, then drop the old one

The key is making database changes backward compatible. I never put code that depends on new schema in the same release as the schema change."

Summary#

Day 3 key takeaways
Deployment strategies
  • Rolling: Gradual replacement, no downtime
  • Blue-Green: Instant switch, instant rollback
  • Canary: Gradual traffic shift with comparison
  • Feature Flags: Decouple deployment from release
Safety mechanisms
  • Health checks before proceeding
  • Metric comparison (canary vs stable)
  • Automated rollback on threshold breach
  • Observation periods between stages
Database migrations
  • Use expand-contract pattern
  • Never make breaking changes directly
  • Separate schema changes from code changes
  • Both old and new code must work during transition
Rollback
  • Code: Deploy previous version (fast)
  • Feature: Turn off flag (instant)
  • Config: Revert configuration (fast)
  • Data: Restore from backup (slow, avoid)
Best practices
  • Deploy frequently (reduces batch size)
  • Deploy early in the week (more support)
  • Monitor during deployment
  • Have a rollback plan before deploying
  • Automate everything possible
Key insight

The safest deployment is a small, incremental one with automated monitoring and fast rollback.

Further Reading#

Books:

  • Book

    "Continuous Delivery" by Jez Humble & David Farley

  • Book

    "Accelerate" by Nicole Forsgren, Jez Humble, Gene Kim

Articles:

  • Read

    Martin Fowler: "Blue Green Deployment"

  • Book

    Google SRE Book: Chapter on Release Engineering

  • Read

    AWS: "Blue/Green Deployments on AWS"

Tools:

  • Read

    Kubernetes: Rolling updates, readiness probes

  • Read

    Istio/Linkerd: Traffic shifting for canary

  • Read

    LaunchDarkly/Split: Feature flag management

  • Read

    Argo Rollouts: Progressive delivery for Kubernetes

You now have the trifecta: Define health (SLOs). See health (observability). Maintain health through change (deployments). Tomorrow, we ensure health under load.

this lesson in shorts · 2

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Day 03 · Deployment Strategies
Finish Day 3Stages clear as you read them (0/4 so far). Skipping ahead doesn't count: marking complete unlocks after ~8 more min of reading.

Go deeper · reading trail

all reading →

Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.

NetflixBlog post · ~8 min

Automated Canary Analysis at Netflix with Kayenta

Michael Graff, Chris Sanden · 2018
What
Netflix's open-source system for judging canary releases automatically.
Key idea
Spinnaker deploys a new baseline cluster and a canary side by side, and Kayenta compares their metrics statistically to pass or fail the release.
Why here
The canary chapter of this lesson made rigorous: compare against a fresh baseline, not the old fleet, and let statistics decide.
taught here: Canary deployment
Dream11Blog post · ~9 min

Deployment At Scale: Story Behind Dream11's In-House Blue-Green Deployment Platform ‘OneClick’.

2022
What
Dream11's automated blue-green deployment platform.
Key idea
OneClick creates the green stack sized from a predicted-concurrency model, then shifts traffic to it gradually instead of all at once.
Why here
Blue-green from this lesson with a twist: size the new stack for upcoming traffic and move traffic in steps.
taught here: Blue-green deployment
StripeBlog post · ~8 min

Online migrations at scale

Jacqueline Xu · 2017
What
How Stripe changes data models under a live payments API.
Key idea
Four steps: dual-write to old and new, change reads to the new path, change writes to the new path, then remove the old data.
Why here
The expand-and-contract database migration of this lesson, done with no downtime on critical data.
taught here: Database migrations

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…