Chapter 16 · Schema Evolution, Migrations, and Compatibility
Backward- and Forward-Compatible Database Changes
Understand backward- and forward-compatible database changes and coordinate deployments when old and new application versions overlap.
Learning outcomes
Application and database deployments rarely change at exactly the same instant. During rolling releases, old and new code may run together. Backward compatibility and forward compatibility describe whether different versions can safely operate against the evolving schema and data.
Define backward and forward compatibility for database changes.
Identify additive and breaking schema changes.
Sequence application and database deployments safely.
Handle mixed-version reads and writes deliberately.
Backward compatibility
A new database/schema version is backward compatible when older application code can continue to operate correctly against it.
Forward compatibility
Forward compatibility asks whether older systems can tolerate data written by newer systems or whether newer code can tolerate older schema/data during transition, depending on context. The practical goal is mixed-version coexistence.
Usually compatible: add nullable column
ALTER TABLE work_orderADD COLUMN priority_code text NULL;Old application code usually ignores it.
Potentially breaking: rename
customer_id -> account_idOld code still sends/queries customer_id. A direct rename can break immediately.
Potentially breaking: tighten nullability
ALTER COLUMN service_region_id SET NOT NULL;If any old writer still omits the field, deployment fails at runtime.
Potentially breaking: new enum value
Old code may use an exhaustive switch:
openscheduledclosedNew database rows containing blocked can crash or misbehave in an old service even though the database change itself is additive.
A schema change can be additive in DDL yet breaking at the application-contract level.
Read compatibility
Old readers should tolerate:
- new nullable fields;
- new reference rows when designed for extension;
- unknown optional JSON fields;
- new columns not selected explicitly.
Write compatibility
New schema should temporarily accept writes from old code. This often means delaying NOT NULL, removing old columns only later, and avoiding new mandatory invariants until old writers are gone.
Rolling deployment sequence
- deploy compatible schema expansion;
- deploy new application code;
- observe mixed-version operation;
- migrate/backfill data;
- retire old application versions;
- enforce new constraints;
- remove old schema elements.
Dual-read transition
When renaming:
read account_idif null: read customer_idThis allows new code to handle records not yet migrated.
Dual-write transition
write customer_idwrite account_idDual writes can maintain compatibility temporarily, but require transactionality or reconciliation to avoid drift.
Database trigger as bridge
A trigger can copy old column to new column during transition, but it creates hidden behavior. If used, document and remove it after migration.
Views as compatibility adapters
A view can expose an old schema shape while underlying tables evolve:
CREATE VIEW legacy_work_order ASSELECT account_id AS customer_id, ...FROM work_order;This can help reports or integrations migrate gradually.
API compatibility and DB compatibility interact
If an API contract changes from Customer to Account terminology, the database does not need to be renamed in the same release. Decouple logical external naming from internal migration timing.
Data format compatibility
Changing:
phone_number: "+49..."to a structured object can break old readers even if stored in JSON. Version flexible payloads too.
Reference-data compatibility
Adding a new status, country code, or failure category can be a breaking data change when consumers assume a closed set. Treat controlled vocabulary evolution like schema evolution.
WorkshopHub mixed-version example
| Phase | Old app | New app | Schema |
|---|---|---|---|
| 1 | Reads customer_id | Not deployed | customer_id + nullable account_id |
| 2 | Still running | Dual reads/writes | Both columns |
| 3 | Retired | Uses account_id | Backfilled both |
| 4 | None | Uses account_id | Drop old customer_id |
Compatibility test matrix
Test combinations:
old app + expanded DBnew app + expanded DBnew app + partially backfilled datanew app + contracted DBPractice: new required status
Add “blocked” status
Old application code throws an exception for unknown status values. Can the database team insert “blocked” immediately?
Review answer
No. First deploy readers that tolerate or understand the new value. Only then allow writers/database processes to produce it. Data-value evolution must respect application compatibility.
Summary and next lesson
Backward and forward compatibility make rolling deployment possible. Additive schema is only one part; data values, nullability, payload formats, and readers/writers must also coexist. The next lesson introduces the standard expand-and-contract migration pattern.
References
- Pramod Sadalage and Scott Ambler, Refactoring Databases.
- Martin Fowler, evolutionary database design articles.
- Martin Kleppmann, Designing Data-Intensive Applications.