Skip to content
About
Main content

Ship a schema change without breaking readers

The decision

A schema change looks like one edit and lands on every reader of the data, including readers several versions behind and data written years ago. Compatibility is a property of the pair (the writer's schema, the reader's schema), never of a schema alone, and a registry only compares the new schema with the previous one. How do you ship this change so that no reader breaks, loudly or quietly?

You will end with a change plan that names the operation, the compatibility mode it needs, the steps and their order, and the checks that no registry will run for you.

Work through these in order.


Work through it

1

Name the operation and test it

A change is safe if and only if it is an information-preserving map over data already written, and its inverse is never required.

  • Add an optional field with a default: safe in both directions.
  • Add a required field: never; old data has no value for it.
  • Drop an optional field: backward only; old readers may still reference it.
  • Widen a type: safe. Narrow a type: never as a schema change; it is a data migration.
  • Rename or reorder: decided by the (step 2).
2

Know how readers find a field

By position, by name or by ID. Under ID identity a rename is only and a drop is safe, provided an ID is never reused. Under name identity a rename is a drop plus an add. Under position, reordering is fatal.

3

Choose the compatibility mode from the deployment order

lets you deploy readers first; lets you deploy writers first, and only works if readers ignore unknown fields. If you do not control the order, you need both. Readers can be more than one version behind, so check against every prior version (transitive), not only the last: A→B and B→C can each pass while A→C breaks. Full transitive is the honest default.

4

Run a rename or a drop as expand/contract

Expand (add the new field), both, old rows, move readers one by one, verify that nobody reads the old field, then (drop it). The last step needs read telemetry, query-log mining or column-level . Without it, the never finishes and dead pile up.

5

If you add an enum value, check the consumers, not the type

A new value passes every registry check and breaks every that matches exhaustively: a CASE with no default, a check constraint, an inner join to a lookup table. Model the value set as a reference table, or require every consumer to carry a visible "unknown" branch.

6

Check where defaults live, and which way nullability moves

A is applied at read time: changing it rewrites the meaning of all historical data, with no file changed and no check triggered. Relaxing required to optional is backward-safe only. Tightening optional to required needs proof that no exist, and that proof is a data check, not a schema check.

7

Ask whether the meaning changed

Compatibility checking compares shapes. A change from cents to dollars in the same integer field passes every mode, parses cleanly everywhere, and makes every downstream number wrong by 100×. Nothing in this mechanism sees it; it needs a contract between and consumers.


Worked example

Rename weight to weight_kg on a table with name identity, read by ten teams. Step 1: a rename under name identity is a drop plus an add, so it is unsafe as one edit. Step 4: expand and dual-write in week 1, backfill in week 2, move readers in weeks 3–6, see no read of weight in the query log for 30 days, drop it in week 8.