Expand/Contract Pattern
The expand/contract pattern (also called parallel change) ships additive schema changes while old and new application versions run, then removes obsolete schema only after traffic fully cuts over.
Search across all documentation pages
The expand/contract pattern (also called parallel change) ships additive schema changes while old and new application versions run, then removes obsolete schema only after traffic fully cuts over.
Phase 1 EXPAND - migration adds new column/table (nullable or unused)
Phase 2 DEPLOY - app v2 writes to both old and new
Phase 3 BACKFILL - job fills new column for historical rows
Phase 4 DEPLOY - app v3 reads new only
Phase 5 CONTRACT - migration drops old column
When to reach for this: Renaming columns, changing types, splitting tables, or any breaking DDL on tables under active traffic.
Rename full_name to display_name without downtime:
-- Migration V10 EXPAND
ALTER TABLE app.users ADD COLUMN display_name text;-- App v2 dual-write (application code)
INSERT INTO app.users (full_name, display_name, ...)
VALUES ($1, $1, ...);
UPDATE app.users SET display_name = full_name WHERE display_name IS NULL;-- Migration V11 BACKFILL (batch in application or SQL job)
UPDATE app.users
SET display_name = full_name
WHERE display_name IS NULL AND id BETWEEN $1 AND $2;-- Migration V12 CONTRACT (after app v3 ships)
ALTER TABLE app.users DROP COLUMN full_name;
ALTER TABLE app.users ALTER COLUMN display_name SET NOT NULL;What this demonstrates:
| Change type | Expand step |
|---|---|
| Rename column | Add new column; dual-write |
| NOT NULL constraint | Add nullable; backfill; then SET NOT NULL |
| Split table | Create new table; trigger or app dual-write |
| Change type | Add amount_cents bigint; migrate from amount numeric |
| FK target change | Add new FK column nullable; backfill; switch |
-- Optional: track migration phase in config table
INSERT INTO app.feature_flags (key, enabled) VALUES ('use_display_name', true);SELECT count(*) WHERE new_col IS NULL gate in deploy pipeline.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Maintenance window DROP | Small internal app | 24/7 SLA |
| Blue/green database | Massive shape change | Team lacks logical replication skill |
| Views as compatibility layer | Rename with many readers | Write-heavy paths through views |
Typically 2-3: expand, dual-write deploy, read-cutover deploy, contract. Tune to your release cadence.
Possible in SQL for uniform enforcement; app dual-write is easier to test. Triggers add write amplification.
True RENAME COLUMN is fast but breaks old app instantly. Expand/contract with new name is safer for zero-downtime.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 19, 2026