How to Do Zero-Downtime Migrations With Expand-Contract
Expand-contract splits a breaking change into additive steps so every deploy leaves old and new code both able to read and write the schema.
The pattern has three phases across separate releases: expand (add the new shape, keep the old), migrate (ship code that writes both and reads new), and contract (drop the old shape once no code uses it). Each phase is independently deployable and reversible.
The three phases
- Expand: add the new column/table/index alongside the old, all additive and backward compatible.
- Migrate: deploy code that dual-writes and reads the new shape; backfill existing rows.
- Contract: in a later release, once no running code references the old shape, drop it.
Release 1: expand (backward compatible)
migration.sql
-- old code keeps using "name"; new column is optional
ALTER TABLE users ADD COLUMN full_name text;Release 3: contract (only after code no longer reads "name")
migration.sql
ALTER TABLE users DROP COLUMN name;Gotchas
- Do not contract in the same deploy that stops using the old shape; a rollback of the code would then hit a missing column.
- Backfill happens in the migrate phase, not the expand phase, so the expand stays fast and lock-free.
Related guides
How to Make Backward-Compatible Schema ChangesKeep schema changes backward compatible so the currently running app version keeps working during a rolling d…
How to Rename a Column Safely (Add, Backfill, Switch, Drop)Rename a column with zero downtime using add, backfill, switch, and drop across several releases, since an in…