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.

Beginner70–100 minutesCompatibility + deployment sequencingLast reviewed: August 2026

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.

01

Define backward and forward compatibility for database changes.

02

Identify additive and breaking schema changes.

03

Sequence application and database deployments safely.

04

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

sql · example
ALTER TABLE work_orderADD COLUMN priority_code text NULL;

Old application code usually ignores it.

Potentially breaking: rename

model · example
customer_id -> account_id

Old code still sends/queries customer_id. A direct rename can break immediately.

Potentially breaking: tighten nullability

model · example
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:

model · example
openscheduledclosed

New database rows containing blocked can crash or misbehave in an old service even though the database change itself is additive.

Compatibility is end-to-end

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

  1. deploy compatible schema expansion;
  2. deploy new application code;
  3. observe mixed-version operation;
  4. migrate/backfill data;
  5. retire old application versions;
  6. enforce new constraints;
  7. remove old schema elements.

Dual-read transition

When renaming:

python · example
read account_idif null:    read customer_id

This allows new code to handle records not yet migrated.

Dual-write transition

model · example
write customer_idwrite account_id

Dual 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:

sql · example
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:

model · example
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

PhaseOld appNew appSchema
1Reads customer_idNot deployedcustomer_id + nullable account_id
2Still runningDual reads/writesBoth columns
3RetiredUses account_idBackfilled both
4NoneUses account_idDrop old customer_id

Compatibility test matrix

Test combinations:

model · example
old app + expanded DBnew app + expanded DBnew app + partially backfilled datanew app + contracted DB

Practice: 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.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.