Schemas and Event Design

In an event-driven system your events are a public API that outlives every service that reads them, so how you shape them and how you evolve them without breaking consumers is not a detail — it is the contract the whole architecture rests on.

Once events decouple your services, the events themselves become the most important contract in the system — more durable than any single service, read by consumers you haven’t written yet. Getting their schema, their evolution, and their design right is what keeps a decoupled system from quietly re-coupling through broken payloads. This sixth post in the Event-Driven Architecture with Kafka series covers schemas, compatibility, and event design.

Events are a contract, and contracts need schemas

To Kafka, a record’s value is just bytes — it does not care what’s inside. That freedom is a trap: without an agreed structure, every consumer guesses at the payload, and a producer changing a field silently breaks consumers it has never heard of. The fix is an explicit schema for each event type, enforced so producers and consumers share a definition. Because events in a durable log are read by many independent consumers over a long time, the schema is effectively a published API — and you evolve it with the same care you’d give a public API, not the casualness of an internal function signature.

The schema registry

The common way to enforce and manage schemas is a schema registry — a service that stores the schema for each topic and assigns versions. Producers register (or validate against) the schema and write a small schema id alongside each record; consumers look up the schema by id to deserialize. This does three things: it enforces that produced records conform, it versions schemas so you can evolve them, and it keeps the payload compact (a schema id, not the whole schema, travels with each record). The registry is what turns “events are a contract” from an aspiration into something enforced at write time.

Serialization formats

Three formats dominate, and the choice is a real trade-off:

The rule of thumb: binary formats (Avro/Protobuf) for high-throughput, strongly-typed pipelines where size and evolution rigor matter; JSON when human-readability and simplicity outweigh efficiency. What matters more than the format is that there is a registered schema with enforced evolution — a schemaless topic is a future outage.

Schema evolution and compatibility

The hard part isn’t defining a schema once; it’s changing it without breaking the consumers and producers that don’t upgrade at the same time. In a decoupled system you cannot deploy everyone atomically, so schema changes must be compatible. Registries enforce compatibility modes:

The practical discipline: make only compatible changes, and let the registry reject an incompatible one at deploy time rather than discovering it when a consumer crashes in production. The changes that break compatibility — renaming a field, changing a type, adding a required field with no default — are exactly the ones to avoid or stage carefully (add-new-then-migrate-then-remove). Treating the schema as an evolvable contract, guarded by the registry, is what lets independent teams ship without a coordinated big-bang.

Event design: what goes in an event

Beyond serialization, what you put in an event shapes how well the architecture works. A key design axis:

Event-carried state transfer is often preferred in EDA precisely because it preserves decoupling — a consumer doesn’t need to call the producer, so the producer can be down and the consumer still works. The trade is event size and the discipline of including the right state. Choose deliberately: notification when payloads are heavy and consumers rarely need full detail; state transfer when decoupling and consumer autonomy matter (usually).

Events versus commands, again

The events-are-facts distinction from the first post shows up in design here too. Model events as things that happened (“PaymentCaptured”), immutable and past tense, owned by the producer — not as instructions (“CapturePayment”) aimed at a consumer. Fact-shaped events keep the producer ignorant of consumers (decoupled); command-shaped ones re-couple. And design each event to be self-describing and self-contained enough that a consumer can act on it without hidden context — which, combined with a registered, compatibly-evolving schema, is what makes your events a durable, trustworthy contract the whole system can build on.

Key takeaways

Further reading

Sources & References

Kafka and schemas