Skip to content

Migration Design ​

Backward-compatible. Staged. Tested in staging. Reversible by default. The four rules every migration honours — designed at shaping time, not improvised at deploy time.

Owners: Tech Lead, Developer Phase it lives in: What We Build → How We Build The corpus principle this enacts: Rollback possible is not. A confirmed time is.

Where it lives in the chain ​

How to do this ​

The migration ADR names, before code begins:

  1. The destination schema — what the model looks like after.
  2. The bridge state — what both old and new code reads during the migration. Both versions must work, simultaneously, against the bridge.
  3. The deploy order — schema first or code first? Adding a column? Schema first. Removing a column? Code first.
  4. The backfill plan — how do existing rows get the new shape? Online (during read), offline (batch job), lazy (on next write).
  5. The cutover signal — what tells the team it's safe to remove the bridge? Usually a metric: zero reads of the deprecated field for N days.
  6. The reversal plan — if the cutover fails, what's the path back? Tested in staging.

What good practice looks like ​

A rename submitted_at → graded_at. Not a single migration. Five PRs:

  1. Add graded_at, dual-write from now on. Backward-compatible. Reversible.
  2. Backfill graded_at from submitted_at for old rows. Idempotent.
  3. Switch reads to graded_at. Old code still works (writes both). Reversible.
  4. Soak for two weeks. Watch the metric: zero reads of submitted_at.
  5. Drop submitted_at. The point of no return — labelled explicitly.

A single PR that renames the column is an outage waiting to happen. The JWT outage shipped because the migration discipline wasn't applied to a config change. Configuration changes are migrations too.

200apps · How We Work · NWIRE