Schema evolution: changing an event's shape without breaking every consumer
An API's versioning problem (API Design's own lesson) has a harder cousin in event-driven systems: there's no request/response handshake to negotiate a version on, a producer has no idea who all is consuming a given topic, and old events already sitting in a Kafka log can't be retroactively rewritten.
Advanced
3 min read
Why this is harder than API versioning
API Design's versioning lesson covered a client and server negotiating a version at request time — a client can ask for v2, or a server can support both versions simultaneously and pick based on a header. An event producer has no equivalent handshake: it publishes once, to a topic with potentially many independent consumers it's never directly aware of, some of which may not even be written yet. Changing an event's shape has to work for every consumer reading it — the ones that exist today, and the ones a different team adds next quarter without the producer's knowledge at all.
The two kinds of compatibility that actually matter
Backward compatibility: a new version of an event schema can still be read by old consumer code that hasn't been updated yet. This is what lets producers deploy a schema change before every consumer has upgraded.
Forward compatibility: old events, already produced before a schema change, can still be read by new consumer code. This matters specifically because old events don't disappear — a Kafka topic with long retention, or a newly-added consumer reading from the beginning of the log, can encounter events produced months before the current schema existed.
The practical rules that keep both directions working
Adding an optional field is almost always safe — old consumers simply never read it (forward-compatible automatically), and new consumers treat its absence in old events as "not present" rather than an error (backward-compatible, as long as the new field isn't treated as required).
Removing a field, or changing its type, is almost never safe — any consumer still expecting that field (by name or by the old type) breaks the instant it encounters an event missing it or shaped differently.
Renaming a field is removal plus addition, not a rename — from every existing consumer's perspective, the old field simply vanished and an unrelated new one appeared; there is no "rename" operation that's inherently safe just because a human thinks of it as one change.
Schema registries: enforcing this automatically instead of by convention
A schema registry (Confluent Schema Registry alongside Kafka, using Avro or Protobuf schemas) stores every version of every event schema centrally and can reject a producer's attempt to publish an incompatible change before it ever reaches the topic — turning "we all agreed to follow these compatibility rules" from a team policy that can be accidentally violated into a build-time or publish-time check that actually enforces it.
Producer tries to publish an event using a schema that
REMOVES a required field the registry has on record →
registry REJECTS the publish, before any consumer ever
sees the incompatible event
Why JSON without a schema registry quietly defers this problem, not avoids it
Plenty of real systems publish events as plain JSON with no registry and no enforced schema at all — this doesn't eliminate the compatibility problem, it just means nothing catches a breaking change until a consumer actually fails on a production message, discovering the incompatibility the expensive way. Using a registry and a strongly-typed schema format (Avro, Protobuf) moves that same discovery to publish time, which is the entire value proposition — not a fundamentally different problem, just a much cheaper place to catch it.