Deployment strategies

Rolling, blue-green, canary, feature flags, and the database migration that has to be compatible with both versions.

6 min read🔁 CI/CD, Linux and Nginx

A deployment is the moment the new version and the old one both exist, and every strategy is a decision about how long that moment lasts, who sees which version during it, and how you get back if the new one is wrong. This repository's own deploy is a worked example of the simplest safe shape; the others are what you add when the traffic, the team or the risk grows.

Rolling: replace them one at a time

Start a new instance, wait for it to be healthy, stop an old one, repeat. Capacity never drops below the old count, and at any moment some fraction of traffic is on the new version. Kubernetes's rolling update (the rollouts lesson) is this with maxSurge and maxUnavailable as the knobs; an autoscaling group with instance refresh is the same on virtual machines.

Its cost is the mixed-version window: for the length of the roll, a request may hit the old version and its retry the new one, a client may get a response shape from the new version and send its next request to the old, and two versions of the application share one database. So a rolling deploy needs compatible versions: the API compatibility rules from the REST course, and the schema rules at the end of this lesson. Rollback is a roll in the other direction, and it takes as long.

Blue-green: two environments, one switch

Two complete environments, blue serving and green idle. Deploy to green, test it — real health checks, smoke tests, even a trickle of internal traffic — then flip the router so green serves and blue goes idle. Rollback is flipping back, in seconds, to an environment that is still warm and still running the old version.

That is what deploy/deploy.sh does on one machine: each deploy is a new timestamped directory under releases/, built and migrated and health-checked while the old release keeps serving; then the current symlink moves, nginx reloads, the API restarts, and a health check confirms. The last five releases are kept, and --rollback moves the symlink back and reloads. It is blue-green with directories instead of servers, and its one gap is the API restart — a few seconds where the Java process is down, which a second instance behind nginx would close.

The cost of blue-green is double the capacity during the deploy (or all the time, if green is kept warm), and the database, which cannot be blue-green: both environments share it, so the schema rule below is what makes the flip-back possible. It is the right default when you can afford the capacity and want a rollback that is instant and certain.

Canary: a little traffic, then more

Deploy the new version to a small slice — one instance, 5% of traffic — watch the error rate and latency against the old version for a fixed period, then widen: 25%, 50%, all. Any regression shows up on 5% of users and is rolled back before the rest ever see it. The observability course's SLO metrics are what you watch, and the comparison must be new versus old at the same time, not new versus last week, because traffic and its mix change.

A canary needs a router that can split traffic by weight — a load balancer with weighted targets, an ingress or mesh with traffic splitting, or, at the application level, a routing rule on a header for internal users first. Kubernetes's rolling update is not a canary: it moves through the pod ratio without stopping to look. Tools like Argo Rollouts and Flagger automate the watch-and-widen loop and the automatic rollback, and the loop is the point: a canary you do not measure is a rolling deploy with a longer name.

Feature flags: deploy is not release

The strategies above deploy code and release it in the same act. A feature flag separates them: the new code is deployed dark, behind a condition, and released by flipping the flag — for internal users, for 1% of customers, for one tenant — without a deployment at all. Rollback is the flag, in milliseconds, and the code stays deployed.

java
if (flags.isEnabled("new-checkout", customer)) return newCheckout.run(cart);
return checkout.run(cart);

The microservices course's configuration lesson covers where flags live (a config service, LaunchDarkly, Unleash, or a table); the discipline this lesson adds is that a flag is debt with a removal date: every flag is two code paths to test and reason about, and a codebase with two hundred stale flags is a codebase nobody can reason about. Flag the risky change, release it gradually, remove the flag and the old path the week after it reaches 100%.

Expand-contract: the database, which cannot roll back

Every strategy above assumes the old and new versions can run against the same schema at the same time, and can be swapped back. A migration that renames a column breaks that in both directions: the old version fails the moment the column is gone, and rolling back the code does not bring the column back. Expand-contract makes every schema change compatible with both versions by splitting it into three deploys:

  1. Expand: add the new column, nullable or with a default. Both versions run; the old one ignores it.
  2. Migrate: deploy code that writes both columns and reads the new one (with a fallback), then backfill the old values into the new column in batches. Both versions still run.
  3. Contract: once no running version reads the old column, remove it. A separate deploy, days later, after the rollback window has closed.

The database course's governance says this in its own words, and this repository's rule for its Flyway migrations is exactly it: expand → migrate → contract on live tables, backward compatible with existing rows, never a rewrite. The migration runs before the switchover in the deploy script for the same reason — a failed migration leaves the old release serving, and a successful one is compatible with it.

Choosing

StrategyRollbackCostsReach for it when
Rollinganother roll, minutesmixed versions during the rollthe default on a platform that does it for you
Blue-greena switch, secondsdouble capacity, a shared databaseyou want certain, instant rollback
Canarystop widening, secondsa weighted router and something to watchthe change is risky and the traffic is large enough to measure
Feature flagflip, millisecondstwo code paths until removeddeploy and release should be different days

They compose: a rolling deploy of flagged code, canaried by tenant, with expand-contract underneath. The one that is not optional is the last one; every other strategy's rollback depends on it.

Progress is saved on this device and to your account when signed in.