pattern

Expand-Contract Migration

also called Parallel Change, Add-Migrate-Remove, N-1 Compatibility

Making a breaking change as a sequence of individually reversible steps - add the new alongside the old, migrate readers and writers across, then remove the old - so that at no point is a rollback destructive.

schema-changedeploymentrollbackcompatibilitymigration

A breaking change made in one step creates a moment where the deployed code and the deployed schema must match exactly, and rolling either one back independently breaks the system. In a distributed deployment where old and new instances run simultaneously, that moment is not a moment but a window, and the window is where the incident happens.

Expand-contract decomposes it: add the new structure without removing the old, move writers, backfill, move readers, and only then remove the old — with each step independently deployable and independently reversible.

Why it matters

The property being bought is that rollback is never destructive. At every intermediate state, both the previous and current versions of the code function correctly against the current schema, so a deployment can be reverted with no data loss and no coordination.

The failure it prevents is specific and common: a schema change shipped together with the code that requires it. Rolling back the code leaves a schema the old version cannot use; rolling back the schema destroys the data the new version wrote. The team is trapped between two bad options during an incident, which is exactly when the trap is discovered.

Implementation patterns

  • Expand: add the new column, table, field or endpoint, nullable or optional, with the old code entirely unaware of it. This step is safe to deploy and safe to revert.
  • Write both: deploy code that writes the old and new representations, still reading the old. Now new data is correct in both places.
  • Backfill: migrate existing rows, in throttled batches, with progress tracked and the job resumable.
  • Read new: deploy code that reads the new representation, still writing both. This is the step to watch closely, and it is revertible because writes are still going to both.
  • Contract: in a later release, stop writing the old and remove it. This is the only irreversible step and should be separated from the others by a period long enough for problems to surface.
  • Every deployed version must tolerate the schema of the version before and after it — the N-1 compatibility rule, which is what makes rolling deployments safe.
  • The same sequence applies to APIs, message formats and file layouts, not only databases: add the new field, produce both, migrate consumers, then remove.
  • Automated checks that a migration is additive, blocking a destructive change that has not gone through the sequence.

Industry example

The pattern is standard practice anywhere continuous deployment meets a relational database, and it is enforced structurally in several ways across the industry: deploy-request workflows in sharded MySQL platforms that gate schema changes and keep them revertible, migration frameworks that refuse destructive operations without an explicit acknowledgement, and API versioning schemes that require additive evolution because a removal cannot be transformed backwards for older clients.

The connection to API versioning is not incidental: a version transformation layer works only if changes are additive, which is the same constraint expand-contract imposes on a schema, arising from the same requirement that old and new consumers coexist.

Failure scenarios

  • Combining steps, especially shipping a schema change with the code that depends on it.
  • Removing the old column in the same release that stops writing it, eliminating the rollback window.
  • A backfill that is not resumable, so an interruption means starting again on a very large table.
  • Backfill unthrottled, degrading production while it runs.
  • Reading the new representation before the backfill completes, so rows migrated later return nulls.
  • Forgetting the non-obvious writers — migration scripts, admin tools, reporting jobs, another service — which continue writing only the old representation.
  • The contract step never happening, leaving both representations forever, which is the most common outcome and produces permanent double-writing and confusion about which is authoritative.

Trade-offs

The sequence takes several releases and therefore several weeks, where a single migration would take an hour. That is a genuine cost in delivery speed, and for a small system with a maintenance window the direct approach is simpler and entirely defensible.

It also requires the code to handle both representations simultaneously, which is temporary complexity that must be cleaned up — and frequently is not, leaving the transitional code in place indefinitely.

The trade is elapsed time and transitional complexity in exchange for every step being independently revertible. For any system that cannot take downtime, or that deploys continuously with old and new instances coexisting, it is not an optimisation but the only correct approach — and the discipline that makes it work is refusing to combine the steps when the schedule is tight, which is exactly when the temptation appears.

Interview question

"We need to rename a column on a table three services write to, with no downtime and continuous deployment. Walk me through every release, and tell me at which point we lose the ability to roll back and what you would do before that point."