A practical lesson on backward, forward, full, and transitive compatibility, including what each model protects and where teams commonly choose the wrong one.
Compatibility models describe which old and new combinations of producers, consumers, and historical data are expected to work during schema evolution. Teams often memorize the terms backward and forward compatibility, then still make poor rollout choices because they do not ask the more important question: compatible with what, and across which time window?
In practice, compatibility is not abstract. It is a deployment and replay property. A model is useful only if it matches how your systems actually evolve. If producers deploy faster than consumers, backward compatibility is usually the daily requirement. If old data must remain readable by new code, forward compatibility matters too. If long-lived retention and replay are common, transitive compatibility often matters more than teams expect.
flowchart TD
A["New producer"] --> B{"Can old consumer read new event?"}
B -->|Yes| C["Backward compatible"]
A --> D["Old event history"]
D --> E{"Can new consumer read old event?"}
E -->|Yes| F["Forward compatible"]
C --> G["Both directions over time may require stronger models"]
F --> G
What to notice:
Backward compatibility means older consumers can still process events produced by newer producers. This is often the most practical day-to-day requirement because producer teams tend to deploy sooner while some consumers lag behind. Additive optional fields are the classic example. A new producer emits an extra field, and old consumers safely ignore it.
Backward compatibility is not enough when consumers must later replay older data into new code. It protects older consumers against newer producers, not the other direction.
Forward compatibility means newer consumers can still process older events. This matters when new consumers are rolled out over retained event history or when old payloads remain in storage and must still be interpreted later. Systems with replay, backfills, and projection rebuilds often need this more than teams first assume.
Forward compatibility usually pushes consumers toward tolerant parsing, sensible defaults, and explicit handling of missing fields.
Full compatibility means both directions hold for adjacent versions. Older consumers can read newer events, and newer consumers can read older events. This is stronger than backward or forward compatibility alone and is often a good target for stable shared contracts, especially when deployments are staggered and historical data remains active.
Still, full compatibility is not the strongest possible model when many versions may coexist. A change can be compatible with the immediately previous version and still fail with data produced several versions earlier.
Transitive compatibility extends the model across multiple versions, not only adjacent ones. This matters when:
For long-lived event histories, transitive compatibility is often the more realistic requirement. A consumer rebuilding a two-year projection is not only talking to the immediately prior schema version.
1compatibilityPolicy:
2 subject: invoice.issued
3 mode: backward-transitive
4 rationale:
5 - producers deploy independently
6 - old projections may replay retained history
7 - downstream consumers are not upgraded simultaneously
This kind of policy definition is useful because it ties the compatibility mode to an actual operating model rather than to generic doctrine.
A common mistake is choosing plain backward compatibility because it sounds safe and familiar, while the real system depends heavily on replay. Another is declaring “full compatibility” but only validating one adjacent version in CI. The label becomes stronger than the actual checks.
Compatibility also becomes weaker when semantic meaning changes are ignored. A change may be technically backward compatible while still being semantically dangerous if older consumers misinterpret the field.
A team chooses plain backward compatibility for an event that feeds several projections rebuilt from one year of retained history. Why might that be too weak?
It may be too weak because backward compatibility mainly protects older consumers from newer producers. A rebuild from retained history also requires new consumer code to understand older events. If replay spans many versions, the stronger question is whether forward or transitive guarantees are required, not just backward compatibility alone.