Databases
Database Migrations
Version-controlled, ordered scripts that change the schema, so every environment reaches the same structure the same way. They make schema change reviewable, repeatable and testable in CI. The production rule is expand-contract: ship additive changes first, migrate the data, and only then remove what the old code needed.
Purpose
A migration is a schema change written down: 001_create_users.sql, 002_add_last_login.sql — applied in order, tracked in a table so the tool knows what has run. It exists because "someone ran ALTER TABLE on prod by hand" is how environments drift apart and deploys start failing in ways staging never showed. With migrations, the schema's history lives in git next to the code that depends on it.
When to Use It
Every schema change goes through one: new tables and columns, index additions, backfills, constraint tightening. They shine in teams — a reviewer sees the exact DDL in the pull request — and in CI, where a throwaway database is migrated from zero to prove the chain still runs cleanly.
Trade-offs
Down-migrations are mostly fiction: reversing DROP COLUMN cannot restore the data, so treat rollback as a new forward migration. On big tables, naive DDL takes locks that block writes for minutes — the operational cost hides in table size. And migrations plus deploys are two moving parts: old code must tolerate the new schema for the minutes (or days) they overlap.
Implementation
Use the stack's tool (Prisma Migrate, Flyway, Liquibase, golang-migrate), one logical change per file, run automatically before deploy. For zero downtime follow expand → migrate → contract: add the nullable column, backfill in batches, make new code write both, then drop the old column a release later. On Postgres, prefer CREATE INDEX CONCURRENTLY and split risky DDL from data moves.