Events Are Contracts Too
Async APIs need the same discipline: an event schema is a contract with every consumer, and evolving it safely means schema compatibility rules and a registry (Avro/JSON Schema).
On this page
This module has treated REST as the API, but modern systems talk asynchronously too - the domain events and
message queues from earlier modules. Here's the lesson teams learn painfully: an event is every bit as much a
contract as a REST endpoint. The moment billing publishes an InvoicePaid event that other services consume,
its shape is a promise, and changing it carelessly breaks consumers you may not even know about. Async APIs need
the same discipline as sync ones - plus a few tools of their own.
Why events feel deceptively safe
A REST response goes to a caller you can picture. An event goes to a topic, and anyone can subscribe - now or in the future. That invisibility makes events feel private and changeable, which is exactly the trap:
// billing publishes this to the "invoices" topic
{ "invoiceId": "INV-42", "orderId": "ORD-7", "amount": "50.00", "currency": "USD", "paidAt": "..." }Three services you've forgotten about consume it: fulfillment triggers shipping, analytics aggregates revenue,
loyalty awards points. Rename amount to amountPaid and all three break silently - no compile error, no failed
HTTP call, just consumers quietly misbehaving. The published event schema is a contract with every current and
future subscriber, and it deserves the same care as a REST payload - arguably more, because the consumers are
even less visible.
Schema compatibility rules
The same breaking/non-breaking distinction from the versioning lesson applies, now governed formally by a schema (Avro, Protobuf, or JSON Schema) with defined compatibility modes:
- Backward compatible - new consumers can read old events (e.g. you added an optional field with a default). New schema reads old data.
- Forward compatible - old consumers can read new events (they ignore the new field - tolerant reader again). Old schema reads new data.
- Full - both directions hold.
The practical rules mirror REST evolution: adding an optional field with a default is safe; removing a field, renaming one, or changing a type is breaking. Serialization formats like Avro enforce this - a field needs a default to be safely addable, and the format is designed so old and new readers coexist.
The schema registry
Who enforces these rules across independently-deployed producers and consumers? A schema registry (e.g. Confluent Schema Registry for Kafka). Producers register a schema version before publishing; the registry rejects an incompatible schema change according to the configured compatibility mode:
producer wants to publish v2 of the InvoicePaid schema
│
▼
Schema Registry: is v2 BACKWARD-compatible with v1?
├─ yes → accept, assign version, allow publishing
└─ no → REJECT - the producer's deploy fails, before any bad event is emittedThis is the async equivalent of consumer-driven contract testing: an automated gate that stops a breaking change at deploy time rather than discovering it via corrupted downstream data. Messages carry a small schema id, so consumers fetch the exact writer schema and deserialize safely even as schemas evolve.
The async-contract discipline, summarized
Treat events like the public API they are:
- Design the event schema deliberately - it's a contract, not an internal dump. Don't serialize your domain entity onto the topic (the same "don't leak the entity" rule as REST).
- Evolve it compatibly - additive changes with defaults; version the schema when you must break it.
- Enforce with a registry - let it reject incompatible changes automatically.
- Make consumers tolerant readers - ignore unknown fields so producers can add them freely.
- Include an event version and a stable event type so consumers can route and adapt.
(There's even an OpenAPI equivalent for events: AsyncAPI, a machine-readable spec for message-driven APIs.)
The invisible-consumer problem makes events riskier, not safer
The intuition that events are 'internal' and safe to change is backwards. With a REST endpoint you can often find callers; with a topic, consumers subscribe independently and may be teams or systems you've never met, so a breaking change fails silently and everywhere at once. Apply MORE compatibility discipline to events, not less - and never treat 'it's just an internal event' as license to change the schema freely.
A REST call is a private letter to one recipient whose address you know - if you change how you write, you can at least tell them. An event is a notice pinned to a public bulletin board in a busy square: you don't know who's reading it, strangers rely on it daily, and newcomers start reading it tomorrow. Tearing down yesterday's notice and posting one with a totally different format silently misleads everyone who depended on the old one. So you post additively (add a line, don't rewrite the existing ones), and the town clerk (schema registry) refuses to let you pin a notice that contradicts the format regulars depend on. The board's visibility to unknown readers is exactly why its format is a stricter contract than a private letter, not a looser one.
Your billing service publishes an InvoicePaid event consumed by fulfillment, analytics, and loyalty. You need to (a) add a paymentMethod field, and (b) rename amount to amountPaid for clarity. A schema registry with BACKWARD compatibility is configured. Which change sails through, which gets rejected, and how do you accomplish the rename without breaking consumers?
Why must an event schema be treated as a contract, with the same care as a REST API?
Key takeaways
- An event schema is a contract with every current and future subscriber - arguably stricter than REST because consumers are invisible and a break fails silently.
- The same breaking/non-breaking rules apply: adding an optional field with a default is safe; removing, renaming, or retyping a field is breaking.
- Schema compatibility modes (backward, forward, full) formalize this, and formats like Avro/Protobuf/JSON Schema enforce safe evolution.
- A schema registry gates producers, rejecting incompatible schema changes at deploy time - the async equivalent of consumer-driven contract testing.
- Design events deliberately (don't leak entities), make consumers tolerant readers, version breaking changes, and describe message APIs with AsyncAPI.