Skip to content
About
Main content

Compatibility modes from deployment order

What do backward, forward and full compatibility each allow, and why must the check be transitive?

Compatibility modes, derived from deployment order

Data is written under schema W (the writer's schema) and read by code that expects schema R (the reader's schema).

A schema registry cannot evaluate pairs, because it does not know R. So it checks the new schema against the previously registered one, and uses that as a proxy for the whole deployed reader population. Keep this in mind. It explains everything below.

The three modes

  • Backward compatible: a new reader can read old data. This allows you to deploy readers first.

  • Forward compatible: an old reader can read new data. This allows you to deploy writers first.

    Note: forward compatibility is largely a property of the reader's parser, not of your schema. If reject unknown fields, the mode is decorative. That is what happened to the mobile app in Ravi's change 1: the schema was on paper, but the app's parser was strict.

  • Full: both. You need it whenever you do not control the , which is the normal condition.

The intersection of both directions is, approximately, one operation: add an optional field with a default. Almost everything else breaks in at least one direction.

Transitive modes

A normal check compares the new schema to the last one. A transitive mode compares it to every prior version. The reason is precise: readers can be more than one version behind.

The classic trap is non-transitive full compatibility: version A to B is safe, B to C is safe, but A to C is broken. And A is the consumer owned by a team that has not deployed since March.

Concrete example.

VersionSchema of the field note
Anote: string, default ""
Bnote removed
Cnote added again: integer, default 0
  • A to B: safe both ways (an old reader uses the default for the missing note; a new reader ignores any note it still finds).
  • B to C: safe both ways (adding an optional field with a default).
  • A to C: broken. Data written under A has note as a string. A reader under C expects an integer. A reader under A reading C data expects a string.

A check against only the previous version passes every step. A check against all versions catches it.

The hard boundary of this whole mechanism

It compares shapes. Ravi's change 3 (cents to dollars, still int64) passes every mode. Every consumer parses the data cleanly. Every number downstream is wrong by a factor of 100. No mechanism here sees that. Semantic change needs a different kind of tool (contracts), and even that only works in part.

The mode you need is a statement about a deployment order you do not control. That is why full transitive is the honest default, and why it still cannot see meaning.