The riskiest moment in most releases is the schema change, and expand-contract remains the technique that makes them boring: add the new structure alongside the old, migrate data and code independently, then remove the old structure only after everything has moved. In 2026, with rolling deployments standard, it is less a technique than a requirement.
Phase one: expand
Add the new column or table as nullable or with defaults, backfill it in batches, and deploy code that reads the new structure while still tolerating the old. Nothing breaks because both shapes coexist.
Phase two: contract
Only after all code paths use the new structure and the backfill is complete do you remove the old column and tighten constraints. The gap between phases absorbs slow rollouts, rollbacks, and forgotten consumers.

Backfills are their own project
Large table rewrites lock or bloat tables if run naively. Batch updates with pauses, progress tracking, and a resumable script turn a risky migration into an observable background job.
The four-phase sequence
Expand adds nullable columns or new tables without touching existing reads. Migrate backfills the new columns in small batches with checkpoints, never one giant transaction. Switch moves reads and writes to the new structure behind a flag or version bump. Contract — only after every instance runs the new code and backfill is verified complete — drops the old columns. Each phase is deployable and reversible, which is what makes the sequence safe under any rollback scenario.
Common violations and their costs
The recurring mistakes: adding a NOT NULL column with a default in one step (locks the table on older databases), renaming in place (breaks every concurrent reader), and running backfill without rate limiting (starves production I/O). The other failure is skipping verification between switch and contract — dropping the old column while a lagging consumer still reads it converts a safe migration into an outage. Automate the phase gates in the migration tooling so the discipline does not depend on memory.

Key takeaways
- Expand, backfill in batches, switch, then contract — always in that order.
- No in-place renames; no table-locking defaults on hot tables.
- Gate contract on verified completion, not on a calendar date.
Final thoughts
Expand-contract is what makes continuous delivery compatible with persistent data, and teams that encode its phases into migration tooling find schema changes dropping from incident reports entirely.