The dangerous migration is not the one that changes the schema. It is the one that assumes every running app instance, job worker, and background consumer will switch expectations at exactly the same instant.
That assumption is almost never safe in production.
What Expand and Contract Means
The pattern works because it accepts overlap between the old world and the new world:
- expand the schema
- keep old code working
- backfill or dual-write
- switch reads and writes
- remove the legacy shape later
For example, moving from full_name to first_name and last_name usually starts with expansion:
ALTER TABLE users ADD COLUMN first_name TEXT;
ALTER TABLE users ADD COLUMN last_name TEXT;
Then you backfill:
UPDATE users
SET first_name = split_part(full_name, ' ', 1),
last_name = substring(full_name from position(' ' in full_name) + 1);
Only after the application reads the new columns safely do you remove full_name.
Why This Pattern Is Worth the Extra Steps
Expand and contract is slower than a one-shot breaking migration, but it handles real production conditions better:
- rolling deploys
- background jobs on older versions
- retries against mixed application versions
- partial backfills that need monitoring
The extra steps are not ceremony. They are what buy you compatibility during change.
Trade-Offs
The cost is temporary complexity:
- more code paths for a while
- migration bookkeeping
- cleanup work later
Teams get into trouble when they do the expand phase and forget the contract phase. If the legacy path never gets removed, the migration becomes permanent architecture debt.
Further Reading