How it works
The explanation, with examples you can run.
The steps, for a column rename
Say users.fullname should become users.display_name.
- Expand. Add
display_nameas a nullable column. Old code ignores it. Deploy. - Dual write. Release code that writes both columns and still reads
fullname. Deploy. - Backfill. Copy
fullnameintodisplay_namefor existing rows, in batches. - Switch reads. Release code that reads
display_name(still writing both, so a rollback is safe). Deploy. - Stop the old writes. Release code that only uses
display_name. - Contract. When no running or rollback-candidate version touches
fullname, drop it, and addNOT NULLor constraints you need.
-- 1. expand: cheap, no table rewrite in PostgreSQL 11+ for a nullable columnALTER TABLE users ADD COLUMN display_name text; -- 3. backfill: small batches, safe to re-runUPDATE users SET display_name = fullnameWHERE id IN (SELECT id FROM users WHERE display_name IS NULL LIMIT 1000); -- 6. contract: only after every running version has stopped using fullnameALTER TABLE users DROP COLUMN fullname;Six steps for a rename feels like a lot. It is the cost of never having a moment where a running version and the schema disagree.
Migrations in the pipeline
- Version every migration in the repository and run it with a tool (Flyway, Liquibase, Alembic, Prisma Migrate, golang-migrate), never by hand.
- Run migrations as their own pipeline step before the code that needs them is deployed, and make each migration safe for the code that's already running.
- Run them first against staging with production-like data volumes; a migration that takes a second on test data can lock a large table for minutes.
- Watch for locks. Adding an index on a busy PostgreSQL table should use
CREATE INDEX CONCURRENTLY; adding a column with a volatile default or changing a column type can rewrite the whole table. Set alock_timeoutso a migration fails fast instead of queueing behind long transactions and blocking everyone. - Keep destructive steps (drops,
NOT NULLon existing data, type narrowing) in a separate, later release.
Why this ties back to rollback
Rolling back code is easy only while the previous version still works against the current schema. Expand-and-contract keeps that true at every step except the last, and you choose when to take the last step.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.