vteh.extratechsolutions

Blue-Green Deployment

Run two identical production environments, deploy to the idle one, and cut over by flipping a router — so release and rollback are a single instant switch, and the database migration is the part that actually bites.

Demo coming soonintermediateblue-greendeploymentreleaserollbackzero-downtimecutover

Every deployment is a moment of maximum risk: you are replacing running, known-good code with new code that has never served real traffic, and you are doing it to the system your customers are using right now. The two questions that decide whether that moment is calm or catastrophic are: how fast does the new version go live, and how fast can I take it back. A rolling restart answers both badly — the new version trickles in over minutes while the old drains, the two run side by side in a version-skewed muddle, and rollback means another slow rolling restart in reverse while the incident is already burning.

Blue-Green Deployment answers both questions with a single mechanism. Run two identical production environments — call them blue and green. One is live and serving all traffic; the other is idle. Deploy the new version to the idle one, take all the time you want to warm it, migrate it, and smoke-test it against production infrastructure while zero users touch it. When you are satisfied, flip the router: all traffic moves from the live environment to the freshly-deployed one in a single switch. The old environment does not get torn down — it stays running, warm and ready, as your rollback. If the new version misbehaves, you flip back in seconds to an environment that was serving perfectly a minute ago.

The problem

Consider a service behind a load balancer, currently on version N, and you need to ship version N+1. The naïve approaches each fail in a specific way:

  • Stop-and-replace. Take the service down, deploy N+1, bring it up. Simple, and it hands your users an outage window on every release — so you release rarely, so each release is huge, so each release is riskier. The thing meant to reduce risk manufactures it.
  • Rolling update. Replace instances one at a time so capacity never drops to zero. No outage, but now N and N+1 run simultaneously for the duration of the roll. Every shared contract — the API a client just called, the database schema, the message format on the queue — must work under both versions at once. And when N+1 is broken, you discover it partway through, with half your fleet on the bad version and a slow reverse-roll standing between you and recovery.

Both couple three concerns that want to be separate: deploying the new code, releasing it to users, and rolling back if it fails. Rolling deployment smears all three across the same slow, half-and-half window. What you actually want is to deploy with no user impact whatsoever, release in one atomic instant, and roll back just as instantly — and to decide release long after deploy is safely done.

The hard part Blue-Green does not magically solve is the one piece of state that is not duplicated: the database. Blue and green are two copies of your stateless application tier, but they read and write the same data. The instant you introduce a schema change, both environments must speak to a schema that satisfies the version currently live and the version about to go live — and, so rollback stays possible, the version you might flip back to. That constraint, not the router, is where Blue-Green deployments actually go wrong.

When to use it

Blue-Green earns its keep when the cost of a bad release is high and the cost of standby capacity is acceptable:

  • You need instant, reliable rollback. The single biggest reason to reach for Blue-Green is that recovery from a bad release is one router flip to an environment that is already running the last good version. If your risk model is dominated by "what happens when a deploy goes wrong," this is the pattern that makes the answer "we flipped back in ten seconds."
  • Releases must not show users a version-skew window. Because traffic moves all-at-once, users never straddle two versions the way a rolling update forces them to. For a UI or an API where mixed versions cause visible breakage, the atomic cutover is worth the extra environment.
  • The application tier is stateless and duplicable. The pattern assumes you can stand up a second identical copy that shares the same backing stores. Twelve-factor services behind a load balancer fit naturally.
  • You can afford roughly double capacity during a release. Two full production environments exist at once. In elastic cloud infrastructure that is a transient cost you pay for minutes; on fixed hardware it may mean permanently provisioning the standby.

When NOT to use it

The pattern's assumptions are also its disqualifiers:

  • You cannot make schema changes backward- and forward-compatible. If a migration must be destructive and instantaneous — rename a column, drop a table, change a type in place — then blue and green cannot both run correctly against one database, and the router flip is no longer reversible. You either adopt the expand/contract discipline (below) or you accept that Blue-Green gives you no real rollback for that release.
  • You want to limit blast radius, not eliminate version skew. Blue-Green sends 100% of traffic to the new version the instant you flip. It does not let 1% of users try it first. When your real fear is "the new version is subtly wrong in a way tests missed," you want Canary — a gradual traffic shift with metrics gating — not an all-at-once switch.
  • Double capacity is genuinely unaffordable. For a large, stateful, or hardware-bound fleet, permanently running a second copy may cost more than the release risk it removes. A rolling update in place is cheaper if you can tolerate its window.
  • The state layer is the thing you are deploying. Blue-Green is a pattern for the stateless tier in front of shared state. It does not deploy a database. Stateful cutover — promoting a replica, switching a primary — is a different and much harder problem, and treating it as "just flip the router" corrupts data.

A useful test: if you cannot describe how a request in flight at the moment of the switch completes safely, and how the database serves both versions across the flip and a possible flip-back, you are not ready to cut over — you have only chosen where the incident will happen.

Architecture

A router — a load balancer, an API gateway, a DNS record, a service-mesh route — sits in front of two identical environments and points at exactly one of them. Both talk to the same shared data stores. Deployment targets the idle environment; release is a change to the router's target; rollback is the reverse change.

flowchart TD
    Users((Users))
    Router{Router — live target}
    Blue[Blue env — v N, LIVE]
    Green[Green env — v N+1, idle]
    DB[(Shared database)]
    Users --> Router
    Router -- 100% traffic --> Blue
    Router -. 0% .-> Green
    Blue --> DB
    Green -. warms, migrates, smoke-tests .-> DB

The release is nothing more than repointing the router. Before the flip, green has already been deployed, migrated, and health-checked against the real database — the switch only changes who receives traffic, never what is running.

sequenceDiagram
    participant Ops
    participant Green as Green (N+1)
    participant Router
    participant Blue as Blue (N)
    Ops->>Green: 1. deploy N+1, run migrations (expand)
    Ops->>Green: 2. smoke-test against prod infra
    Green-->>Ops: healthy
    Ops->>Router: 3. flip target → Green
    Note over Router: all new traffic now hits N+1
    Ops->>Blue: 4. drain in-flight, keep warm as rollback
    Note over Ops: soak. If bad → flip Router back to Blue.

Two things make this safe rather than merely fast. First, draining: at the instant of the flip, blue may still be finishing requests. The router must stop sending new connections to blue while letting existing ones complete, so no user is cut off mid-response. Second, the soak: blue stays fully running after the flip — not decommissioned — for as long as your confidence in N+1 takes to build. Only once N+1 has proven itself does blue become the next deployment's idle target. Tear blue down too early and you have thrown away the rollback that was the entire point.

Code walkthrough

Blue-Green is an operational pattern, but the release itself is a small state machine, and modelling it as one — rather than as a shell script of imperative steps — is what keeps rollback correct under pressure. Here is a dependency-free release orchestrator in TypeScript. Start with the domain vocabulary.

type Slot = "blue" | "green";

interface Environment {
  deploy(version: string): Promise<void>;
  migrate(version: string): Promise<void>;
  healthCheck(): Promise<boolean>;
  drain(): Promise<void>;
}

interface Router {
  liveSlot(): Promise<Slot>;
  switchTo(slot: Slot): Promise<void>;
}

Environment is one of the two identical stacks; Router is whatever fronts them — a load balancer API, a gateway route, a DNS client. Keeping both as ports is what lets the orchestrator be pure policy: it knows the choreography of a safe cutover and nothing about AWS, Cloudflare, or nginx.

The release moves through explicit, named states. A discriminated union makes the illegal states unrepresentable — you cannot "roll back" a release that never flipped, because that value cannot be constructed.

type Release =
  | { phase: "idle"; live: Slot }
  | { phase: "deployed"; live: Slot; standby: Slot; version: string }
  | { phase: "verified"; live: Slot; standby: Slot; version: string }
  | { phase: "live"; live: Slot; previous: Slot; version: string }
  | { phase: "rolled-back"; live: Slot; version: string };

const other = (slot: Slot): Slot => (slot === "blue" ? "green" : "blue");

Now the orchestrator. Each method is a legal transition; each returns the next state rather than mutating a flag, so the history of the release is in the type, not in your head at 3 a.m.

class BlueGreenRelease {
  constructor(
    private readonly envs: Record<Slot, Environment>,
    private readonly router: Router,
  ) {}

  async deploy(from: { phase: "idle"; live: Slot }, version: string): Promise<Release> {
    const standby = other(from.live);
    await this.envs[standby].deploy(version);
    await this.envs[standby].migrate(version);
    return { phase: "deployed", live: from.live, standby, version };
  }

Deployment and migration happen on the standby slot while from.live keeps serving every user. Note the ordering: the expand-phase migration runs here, before any traffic moves, against the shared database — which is exactly why it must be backward-compatible with the still-live old version (see below).

  async verify(state: Extract<Release, { phase: "deployed" }>): Promise<Release> {
    const healthy = await this.envs[state.standby].healthCheck();
    if (!healthy) throw new Error(`standby ${state.standby} failed health check; not switching`);
    return { phase: "verified", live: state.live, standby: state.standby, version: state.version };
  }

  async cutOver(state: Extract<Release, { phase: "verified" }>): Promise<Release> {
    await this.router.switchTo(state.standby);
    await this.envs[state.live].drain();
    return { phase: "live", live: state.standby, previous: state.live, version: state.version };
  }

verify is the gate: a standby that fails its health check cannot advance to verified, and only a verified state is accepted by cutOver, so the type system forbids flipping traffic to an unverified environment. cutOver switches the router first, then drains the now-previous environment — new connections land on N+1 while in-flight requests on N finish cleanly. Crucially, previous is retained: the old slot is still running.

  async rollback(state: Extract<Release, { phase: "live" }>): Promise<Release> {
    await this.router.switchTo(state.previous);
    await this.envs[state.live].drain();
    return { phase: "rolled-back", live: state.previous, version: `pre-${state.version}` };
  }
}

Rollback is cutOver run backward, and it is cheap and fast precisely because previous was never torn down. It is still warm, still on the last known-good version, still connected to a database that — if you did the migration right — it can still read. The whole design exists to make this method a few seconds of router change instead of a fresh redeploy during an incident.

const release = new BlueGreenRelease({ blue, green }, loadBalancer);

let state = await release.deploy({ phase: "idle", live: "blue" }, "2.4.0");
state = await release.verify(state);   // green is healthy against prod infra
state = await release.cutOver(state);  // one flip: all traffic → green, blue drains
// soak… metrics look wrong:
state = await release.rollback(state); // one flip back → blue, seconds later

The lesson is not the specific transitions; it is that release and rollback are the same primitive — a router switch between two live environments — and modelling them as explicit states keeps the "can I still go back?" invariant honest instead of buried in operational memory.

The database: expand and contract

Everything above assumes blue and green can share one database across the flip and the flip-back. That is only true if every schema change is applied as a sequence of individually backward-compatible steps, never a single breaking one. The discipline is expand/contract (also called parallel-change), spread across more than one release:

  1. Expand. Add the new schema additively — a new nullable column, a new table, a new index. The old version ignores it; the new version can use it. Both run.
  2. Migrate & backfill. Have the new code write both old and new shapes, or backfill existing rows, so the data satisfies both readers.
  3. Contract — a later release. Only once no running version reads the old shape do you remove it, as its own expand-safe deployment.

The rule that makes rollback real: never destroy in the same release that introduces the replacement. If green's migration drops the column blue still reads, then the moment you flip, blue can no longer serve — and your instant rollback is gone exactly when you need it. A destructive migration turns Blue-Green's greatest strength into a trap. The router flip is reversible; a DROP COLUMN is not. Keep the two apart by at least one release, and the standby stays a valid rollback target.

Performance characteristics

The metrics above are indicative — orders of magnitude to build intuition, not measurements from a specific system.

The cutover and rollback are both O(router change), not O(fleet size). Because the new environment is fully deployed and warmed before any traffic moves, going live costs one configuration change — a DNS weight, a load-balancer target swap, a gateway route update. This is the pattern's defining property: release time and rollback time are decoupled from how big your fleet is or how long the app takes to boot. A rolling update's recovery time scales with the fleet; Blue-Green's is flat and small.

You pay for it in capacity, in a shape that depends on your infrastructure. During a release two full environments exist. On elastic cloud, that doubling lasts only the minutes between deploy and decommission and costs almost nothing amortized. On fixed hardware, the standby may sit provisioned year-round — a permanent capacity tax in exchange for the release safety. Know which regime you are in before you assume the cost is trivial.

Warm-versus-cold is the hidden win. Because green serves its very first user request only after it has been health-checked and given time to prime caches, JITs, and connection pools, users move from one fully-warmed environment to another. There is no cold-start latency cliff at release time the way there is when fresh instances take live traffic immediately — provided you actually soak green before the flip rather than flipping the instant deploy finishes.

Live demo

An interactive Blue-Green playground is coming soon. You will be able to deploy a new version to the idle environment, watch it warm and pass its health gate while the live one keeps serving, flip the router and see traffic move all-at-once with in-flight requests draining cleanly — then trigger a bad release and feel how fast a rollback is when the previous environment is still running. A destructive-migration mode will let you break the rollback on purpose to see why expand/contract matters. Check back shortly.

Related patterns

Canary Release is Blue-Green's closest sibling and its sharpest contrast. Both keep two versions running; the difference is the shape of the traffic shift. Blue-Green flips 100% of traffic in one instant — optimising for a clean, skew-free cutover and instant rollback. Canary moves traffic gradually — 1%, then 10%, then 50% — gating each step on live metrics, optimising for a small blast radius when the new version is subtly wrong. Choose Blue-Green when your fear is "the deploy mechanics fail and I need to get back fast"; choose Canary when your fear is "the new code is quietly bad and I want few users exposed while I find out." Mature pipelines use both: Blue-Green environments as the substrate, Canary weighting as the release policy on top.

API Gateway is the most natural place to put the router. The gateway already terminates client traffic and routes it inward, so the blue/green switch becomes a route-weight change at a component you already run — no separate load-balancer dance. Making the cutover a gateway config change also means the same place that does auth, rate-limiting, and observability owns the release switch, so you can watch the flip's effect on latency and error rate at the exact boundary where it happens.

Service Discovery is what keeps the two environments addressable and the switch honest in a dynamic fleet. When blue and green register and deregister their instances, the router's notion of "the live slot" resolves through the registry rather than through hardcoded addresses — so draining a slot means deregistering it, and the health checks that gate the cutover are the same health signals the registry already tracks. In a service-mesh deployment the flip is often literally a change to which subset the discovery layer marks as live.

Related patterns

Discussion