Database Migrations: How to Ship Schema Changes Without

A database migration is a versioned change to your database schema or data — added columns, renamed tables, backfilled values.

Database Migrations: The Deploys Where 'Undo' Doesn't Really Exist

A database migration is a versioned, reproducible change to your database schema or data — adding a column, renaming a table, backfilling values, changing a constraint. Unlike application code deploys, migrations are stateful: rolling back means either running a reverse migration (rare, usually incomplete) or restoring from backup (slow, disruptive). The asymmetry between deploy and rollback is why database changes deserve more care than application changes, not less.

The expand-contract pattern

The standard technique for zero-downtime schema changes is expand-contract (also called parallel change). To rename a column: (1) Expand — add the new column, dual-write from application code so both old and new stay in sync. (2) Migrate — backfill historical data into the new column. (3) Verify — reads and writes go against the new column, but old still populated. (4) Contract — remove writes to the old column, then remove the old column. Each step is independently deployable and reversible. Doing all steps in one migration is where outages come from.

Long-running migrations

Any migration that acquires a table lock for more than a few seconds is dangerous on a live database. Postgres examples: ALTER TABLE ADD COLUMN with a default value (in older versions) rewrites the whole table; adding an index without CONCURRENTLY blocks writes; changing a column type often rewrites. Rules that prevent most outages: never run a schema change that locks a large table in a single transaction; use CREATE INDEX CONCURRENTLY; batch data migrations into 1000-10000 row chunks with commits between; run heavy migrations off-peak with monitoring.

Migration tooling

Every mature stack has migration tooling — Alembic (Python), ActiveRecord (Rails), Prisma Migrate (Node), Flyway (JVM), Supabase migrations (Postgres). Requirements for any tool: (a) migrations are files in version control, reviewed via PR. (b) applied in strict order, tracked in a metadata table. (c) reproducible against a fresh database. (d) previewable (generate SQL without executing). Rolling your own migration mechanism is a common early mistake that becomes expensive to replace once you have production data.

Testing migrations

Migrations should be tested against realistic data volumes, not empty local databases. A migration that runs in 200ms locally may take 40 minutes on production data. Common approach: maintain a staging database that's a scrubbed snapshot of production, run migrations there first with timing measured, then apply to production. Never apply a migration to production that hasn't run successfully in a staging environment with similar data volume.

The irreversibility contract

Dropping columns and tables is irreversible in practice — even with backups, the operational cost of restore is high, and any writes since the drop are lost. Best practice: never drop in the same PR that stops using; wait one full deploy cycle (or one week, whichever is longer) between 'application no longer reads this' and 'schema drops this.' The gap gives you room to notice mistakes when the cost of recovery is a code revert, not a database restore.

Frequently asked questions

Should migrations run automatically on deploy?
For additive migrations (new tables, new columns with defaults, new indexes concurrent), automatic on deploy is fine and reduces coordination overhead. For destructive or long-running migrations, gate behind manual approval — the deploy pipeline runs the schema check, but a human triggers the migration.
How do we handle migrations across many microservices?
The service that owns the database owns the migration. Cross-service migrations (schema change that affects multiple services) require choreographed rollout: expand the schema first, deploy all consuming services to handle both old and new, then contract. Attempts to migrate schema and consumers simultaneously produce brief broken windows.
What about migrating between database systems?
That's a project, not a migration — plan for months, not weeks. Standard approach: dual-write to both systems, backfill history to the new system, verify parity, switch reads, then switch writes, then decommission old. Big-bang cutovers between database systems are one of the most reliable ways to have a very bad quarter.

Related fundraising guides (40)

Investor directory · Fundraising library · Articles A–Z · Company funding database