Skip to content
AB

Database Migrations Are Executable Architecture

Treat schema changes, constraints, backfills, compatibility, and cutovers as reviewed application design rather than deployment chores.

Ashish BhakhandPublished

Application architecture is not only Python modules. Unique constraints, ownership rules, triggers, indexes, and stored procedures decide which states the system can represent.

A migration is executable architecture because it changes those rules for live data.

Write the invariant first#

Instead of “add an index,” state the guarantee:

A user can have at most one daily recovery score for a local date.

Then encode it:

Code
create unique index if not exists computed_scores_user_date_unique
on public.computed_scores (user_id, score_date);

The application may check before inserting for a friendlier error, but the database arbitrates concurrent writes.

Separate shape, data, and cutover#

A safe change often has phases:

  1. add a nullable column or new table;
  2. deploy code that can read old and new representations;
  3. backfill existing data in bounded batches;
  4. verify completeness and correctness;
  5. add stricter constraints;
  6. remove obsolete reads and columns later.

A hard cut is appropriate when all clients and deployments can change together. Compatibility is valuable only when there is a real mixed-version window.

Make reruns safe#

Use guards such as if not exists, deterministic updates, and conflict handling where they preserve the intended result. A migration that fails halfway should have a documented recovery path.

Idempotent does not mean hiding every error. A changed constraint with the same name should fail rather than pretending the desired schema exists.

Test important migration behavior#

Tests can assert that critical migrations contain the expected constraint, policy, or function definition. Stronger integration tests apply migrations to a representative database and verify behavior with conflicting writes and legacy rows.

Review data volume, lock duration, index build behavior, and rollback options. A syntactically correct statement can still create an outage on a large table.

Schema design controls correctness after application bugs, retries, and concurrent requests. Treat migrations with the same care as the services that depend on them.

← Back to all writing