Chapter 2 of 11 · 5 checks · 3 min
Backward compatibility: expand and contract
Checks that both the old and new versions of the application keep working at every step, because during a rolling deploy both are running.
The currently deployed application works against the migrated schemaMust
Why it matters and how to check
Migrations usually run before or during the deploy, so for minutes or hours the old code talks to the new schema. If the migration drops a column the old code reads, adds a
NOT NULLcolumn it does not write, or renames a table, requests fail until the deploy finishes, and the rollback path is broken too.To verify: in staging, apply the migration and keep the previous release running against it. Run its smoke tests or end-to-end tests. Any failure means the change needs to be split into expand and contract steps.
Renames and type changes use expand, migrate, contract instead of one stepMust
Why it matters and how to check
Parallel change, also called expand and contract, makes a backward-incompatible change in three phases: add the new form alongside the old one, move every reader and writer to the new form, then remove the old one once nothing uses it. For a column rename that means: add the new column, write to both, backfill, read from the new one, stop writing the old one, then drop it.
To verify: for any rename, type change or table split, the plan should show each phase as its own release or deploy step, with the code change that goes with it. A plan with
RENAME COLUMNin the same deploy as the code that uses the new name fails this check.Example
SQL -- release 1 (expand): new column, app writes both ALTER TABLE users ADD COLUMN email_address text; -- between releases: backfill in batches (see the backfill chapter) -- release 2: app reads email_address, still writes both -- release 3: app stops writing email -- release 4 (contract): nothing references the old column ALTER TABLE users DROP COLUMN email;Columns and tables are dropped only after no deployed code references themMust
Why it matters and how to check
Many ORMs list every column they know about in
SELECTandINSERTstatements, so removing a column the model still declares breaks queries even when the code never uses it. Drop in a later release than the one that removes the field from the code and the model.To verify: search the code for the column or table name, including raw SQL, reports and background jobs. Confirm the release that removed it from the model is fully deployed, and check the database's statistics or logs for recent queries that still reference it before running the drop.
New required columns get a default or are filled before NOT NULL is enforcedMust
Why it matters and how to check
Adding a column as
NOT NULLwithout a default fails on a table with rows, and adding it with a default the old code does not know about is fine only if old inserts still succeed. The safe order is: add the column as nullable or with a constant default, deploy code that writes it, backfill old rows, then enforceNOT NULL.To verify: for each new non-nullable column, check that old application code can still insert rows (the default covers it, or the column stays nullable until the new code is everywhere). Confirm the
NOT NULLstep comes after the backfill, using one of the low-lock methods in the PostgreSQL chapter.New enum values, lookup rows and constraints are added before code depends on themShould
Why it matters and how to check
If the new code writes a status the database enum does not have yet, or inserts a reference to a lookup row that is not there, inserts fail during the deploy. Equally, a new constraint that old code violates breaks the old code.
To verify: check the order. Allowed values and lookup rows should ship in a migration before the code that writes them. New constraints should ship after every writer complies, and old data should be checked against them first.