Practice a release rollback with an explicit schema boundary

A code rollback can fail when the database has already changed in a way that the older release cannot read. Write the compatibility boundary before deployment: which application revisions can use the new schema, and which data transformations would be difficult to reverse?
Rehearse an additive change
Consider a synthetic customer table whose original application reads display_name. A new release needs preferred_name. During the first migration, keep the old column and add the new one with a compatible empty state. The conceptual sequence is:
Release A: reads display_name
Migration 1: adds nullable preferred_name
Release B: reads preferred_name with display_name fallback
Backfill: fills preferred_name from reviewed source values
Later release: removes old readers before old column removal
This is a design example, not an executed migration. Decide how new writes reach both representations while old and new application versions can run at the same time. A backfill that handles existing rows once does not keep later writes synchronized.
The old release stays compatible during the additive phase; removing the old field crosses a later rollback boundary.
Test the older release after migration
Create a disposable database with synthetic rows. Apply the additive migration, start the older application revision, and exercise a read and a write that depend on the changed table. Then run the newer revision against the same fixture. Record any worker process or administrative command that still assumes the old structure.
Django's migration documentation explains dependencies and reversible operations, but a formally reversible migration can still discard values during reversal. For example, removing a new column destroys data stored only in that column. A rollback plan must say whether that loss is acceptable and whether a restore or forward repair would be required.
Set an explicit stop condition
If a release has already written a representation the older code cannot understand, stop before switching binaries. Preserve the current state and use the documented repair path. Avoid trying a reverse migration on production merely to see whether it succeeds.
Schema locks and a large backfill can affect availability even when the logical change is additive. Measure those effects with representative data in a separate environment. Include queue workers in the release boundary because an older worker can keep writing old-format values after the web process changes.
Reference: Django 5.2 migrations. The engineering blog includes related release checks. No production migration, backfill or rollback was performed for this example.