Free — no signup required

The "Expand-Contract" Pattern (Parallel Change)

3 min read

The Core Idea

The Expand-Contract pattern (also called Parallel Change) is the standard technique for making breaking schema changes without downtime. Instead of one destructive step, you break the change into three phases: Expand (add the new structure), Migrate (transition data and code), and Contract (remove the old structure). Each phase is a separate, safe deployment.

Let's walk through a concrete example: splitting a single full_name column into first_name and last_name.


Phase 1: Expand (The Safe Migration)

Run a migration that adds the new columns — first_name and last_name — as nullable with no default. Do not touch full_name.

This migration is safe to run against the live database. The old application code doesn't know these columns exist and ignores them. No queries break. No locks are held for long.

Deploy this migration. Do not deploy new application code yet.


Phase 2: Write to Both + Backfill (The Transition)

Deploy a new version of the application that writes to all three columns on every insert and update, but still reads from full_name. On every user creation, the application now splits the incoming full name and writes all three columns in the same insert — this ensures that from this point forward, new data is captured in both formats.

Simultaneously, run a background backfill job that derives first_name and last_name from the existing full_name value for every row where they're still null, run in small batches (e.g., 1,000 rows at a time with a short sleep between batches, repeated until zero rows remain) so the update doesn't hold a long lock on the table.


Phase 3: Read from New (The Switch)

Once the backfill is complete and verified (zero rows with a still-null first_name), deploy a new application version whose read path pulls from first_name and last_name instead of full_name. It may still write to full_name as a safety net, or stop writing to it — your choice based on rollback risk tolerance.

At this point, full_name is unused by the application but still exists in the schema.


Phase 4: Contract (The Cleanup)

After the new code has been stable in production for a sufficient observation period (days or weeks, depending on your risk tolerance), run the final destructive migration that drops the now-unused full_name column.

This is now safe because no running code references full_name. The column is gone, and the schema is clean.

Key insight for senior engineers: The "observation period" before Phase 4 is not optional. Rollback from Phase 3 to Phase 2 is easy — the old column still exists. Rollback after Phase 4 requires a data restore. The cost of waiting a week is low; the cost of premature cleanup is potentially catastrophic.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free