Tags: web-dev concept

Database Migrations

Date: 2026-08-16


Changing schema while the old code is still running. The rule that makes it survivable is that every change must be backwards-compatible at the moment it lands — which turns most “rename a column” tasks into a sequence of four deploys.


What it is

A database migration is a versioned, repeatable change to schema or data, applied as part of deployment.

The hard constraint: during any deploy — and throughout any progressive rollout — old and new application code run simultaneously against one database. So the schema must satisfy both.

Expand and contract

The pattern that makes this work. Renaming email to email_address:

1  EXPAND      add email_address, nullable. Deploy.
               old code ignores it. Nothing breaks.

2  BACKFILL    copy email → email_address in batches.
               dual-write both columns from application code.

3  MIGRATE     deploy code reading email_address, still writing both.
               verify. this is the point of no return, and it's reversible
               until you take step 4.

4  CONTRACT    stop writing email. Deploy.
               later, in a separate release, drop the column.

Four deploys to rename a column. That’s the cost of never having downtime and always being able to roll back.

Step 4 is the one to delay. Dropping the column is irreversible and there’s rarely urgency. Leaving it a fortnight costs nothing and buys a clean rollback path for everything before it.

Safe and unsafe operations

OperationSafe during deploy?
Add a nullable columnYes
Add a column with a defaultDepends — on some engines and versions this rewrites the whole table
Add an indexConcurrently, yes. Without, it locks writes
Drop a columnOnly after nothing references it
Rename anythingNever directly. Expand-contract
Change a column typeRarely direct. New column, backfill, swap
Add a NOT NULL constraintOnly after backfilling and validating separately
Add a foreign keyAdd as NOT VALID, then validate separately

[CHECK: which operations lock and for how long is engine- and version-specific — verify against your database’s documentation before running anything on a large table.]

The table size is what decides risk. Every operation above is instant on 10,000 rows and can lock a production table for minutes on 50 million.

Backfills

Never in the migration itself. A migration that updates ten million rows in one transaction holds locks, bloats the transaction log, and can’t be interrupted.

batch of 1,000 → commit → brief pause → repeat
  resumable, interruptible, doesn't hold locks
  monitor replication lag and back off if it grows

Run it as a background job with progress recorded, so it survives a restart.

Rules

  • Migrations are code. In the repo, in version control, reviewed, applied by the pipeline
  • Forward-only in production. “Down” migrations are appealing and rarely correct — reversing a schema change that’s already had data written against it usually loses data. Roll forward with a new migration — Rollback and Forward Fix
  • Never destructive in the same release as the code change. Separate them so the code can be reverted independently
  • Test against production-scale data. A migration that runs in 40ms locally can lock a table for four minutes against real volume
  • Set a lock timeout, so a migration that can’t acquire a lock fails fast instead of queueing every write behind it
  • One logical change per migration. Easier to reason about, and easier to skip if one fails

Where it interacts

  • Progressive Delivery — during a ramp, both code versions are live, so the schema must serve both. This is the constraint that makes expand-contract mandatory rather than merely good practice
  • Feature Flags — a flag can gate which columns the code reads, but it can’t gate the schema. The migration always ships first
  • Site Migrations and SEO — a replatform is a data migration with a URL migration attached, and both need mapping
  • Backwards Compatibility — the general principle this is a specific case of