Schema Evolution and Consumer Compatibility
LESSON
Schema Evolution and Consumer Compatibility
By the end of this lesson, you will be able to...
Classify event schema changes as safe, risky, or breaking for independent consumers.
Design a compatibility path for an event that must change while old consumers still run.
Review the difference between syntactic compatibility and semantic compatibility.
Idea in one sentence: An event schema is a long-lived promise, so changing it safely means protecting consumers you do not deploy with the producer.
Core Insight
An order service publishes OrderPlaced.
Three consumers use it:
fulfillment-service -> reserves stock
email-service -> sends a confirmation
analytics-pipeline -> builds revenue reports
The producer team wants to improve the event.
The first version looks like this:
{
"event_id": "evt-901",
"event_type": "OrderPlaced",
"order_id": "O-812",
"customer_id": "C-44",
"total_cents": 4999
}
Now the business starts selling in multiple currencies. The producer team wants to publish:
{
"event_id": "evt-901",
"event_type": "OrderPlaced",
"order_id": "O-812",
"customer_id": "C-44",
"amount": {
"value": 4999,
"currency": "USD"
}
}
That looks cleaner.
It may also break every consumer that reads total_cents.
The producer did not change a local helper function. It changed a contract in motion. Events are especially sensitive to this because producers and consumers are deployed independently. A producer can publish a new shape today while one consumer is still running code from last month.
Schema evolution is the discipline of changing event contracts without surprising independent consumers. The hard part is not only whether a parser accepts the bytes. The hard part is whether the consumer can still make the same business decision.
The Naive Idea
The naive idea is:
The producer owns the event, so the producer can change the event when its code changes.
That idea works inside one deployable unit. If one service owns both the writer and all readers, it can update them together.
It breaks in an event-driven system.
The producer owns the fact that the order was placed. It does not own all the ways that fact is used. Fulfillment may depend on the order id. Email may depend on the customer id. Analytics may depend on money fields. A fraud model may subscribe later and read old events from the log.
So the event is not just a data structure.
It is a published promise.
Plain meaning:
Compatibility means old and new code can still work during a change.
In this scenario:
The order service can add useful information without forcing fulfillment, email, and analytics to deploy at the same minute.
Technical name:
That promise is consumer compatibility. Schema evolution is the set of rules and release steps that preserve it.
Where Compatibility Breaks
A schema can be compatible at one layer and broken at another.
Syntactic compatibility asks:
Can the consumer decode or parse this event?
Semantic compatibility asks:
Can the consumer still make the same meaning from this event?
Those are different questions.
Suppose the producer changes total_cents from an integer to a string:
{
"total_cents": "4999"
}
Some consumers may parse it. Others may fail. This is a syntactic risk because the field type changed.
Now suppose the producer keeps the integer type but changes the meaning:
{
"total_cents": 4999
}
Before, total_cents meant "price paid by the customer after discounts." After the change, it means "catalog subtotal before discounts."
Every parser may still work.
Analytics is now wrong.
That is a semantic break. It is more dangerous because the system may not fail loudly. It may produce plausible but false reports.
Check: A producer keeps a field named customer_id, keeps it as a string, but changes it from the account id to the shipping-contact id. Is this compatible?
Think first, then reveal.
Answer: It is syntactically compatible, but semantically risky or breaking. Existing consumers may still parse the value, but they may join it to the wrong customer records or apply the wrong business rules.
A Compatibility Review Table
Use a small review table before changing an event.
The table should make four things visible:
input -> proposed change -> consumer interpretation -> decision
Here is the OrderPlaced change reviewed step by step.
| Proposed change | Old consumer sees | New consumer sees | Compatibility decision | Naive failure contrast |
|---|---|---|---|---|
Add optional currency with default assumption of USD |
Ignores unknown field and keeps using total_cents |
Reads currency when present | Usually safe if old meaning stays true | A naive producer may assume adding any field is always safe, but consumers can still fail if they reject unknown fields. |
Rename total_cents to amount.value immediately |
Missing total_cents |
Reads structured amount | Breaking | A naive producer sees a cleaner model; old consumers see missing data. |
Publish both total_cents and amount for a transition window |
Keeps reading total_cents |
Moves to amount |
Safer migration path | A naive one-step replacement forces every consumer to move at once. |
Change total_cents meaning from paid total to subtotal |
Parses the same field | Parses the same field | Semantic break | A naive parser check passes, while business meaning changes under consumers. |
Add schema_version: 2 and document the new money model |
Can route or reject if needed | Can select the new contract | Useful, but not enough alone | A naive version field does not protect consumers unless release rules and meanings are clear. |
The output of this review is not "use versioning" or "never change events."
The output is a migration plan.
For the money change, a safer path is:
1. Keep publishing `total_cents` with its old meaning.
2. Add `amount.value` and `amount.currency`.
3. Document that new consumers should read `amount`.
4. Watch which consumers still read `total_cents`.
5. Remove `total_cents` only after the event retention window and consumer migration policy allow it.
The transition costs more than the one-step change. It also avoids converting independent deployment into a hidden lockstep release.
Designing the Change
A useful event compatibility policy separates changes into common buckets.
Usually Safe
These changes are usually safe when consumers ignore unknown fields and the new fields are optional:
- Add a new optional field.
- Add a new enum value only if consumers have a known fallback path.
- Add metadata that does not change the business meaning of existing fields.
- Publish a new event type for a genuinely new fact.
"Usually" matters. If a consumer uses strict decoding and rejects unknown fields, even an added field can break it. If a consumer treats every enum value as exhaustive, a new value can crash it or send work to the wrong branch.
Risky
These changes need review, migration, or consumer evidence:
- Add a required field with no default.
- Change units, time zones, precision, or rounding rules.
- Change when the event is emitted.
- Change whether the event means "requested," "accepted," "committed," or "completed."
- Split one event into several events.
These changes often preserve the rough shape of the data while changing the consumer's decision.
Breaking
These changes should usually require a new versioned contract or a planned migration:
- Remove a field used by consumers.
- Rename a field without publishing both names for a transition.
- Change a field type in a way old consumers cannot decode.
- Reuse an event type for a different business fact.
- Reinterpret an existing field with a different meaning.
The main design question is not:
Can the producer generate this new shape?
The better question is:
Can every supported consumer either keep working, safely ignore the change, or deliberately reject it?
Consumer-Driven Compatibility
The producer cannot guess every consumer's use of an event.
So compatibility needs evidence.
Consumer-driven contract review asks each important consumer to state what it depends on:
| Consumer | Depends on | Decision made from event | Break signal |
|---|---|---|---|
| fulfillment-service | order_id, line items, purchasable status |
reserve stock | reservation failures or rejected messages |
| email-service | customer_id, locale, order summary |
send confirmation | missing template data |
| analytics-pipeline | amount, currency, discount meaning | revenue reporting | metric jump or reconciliation drift |
This table changes the conversation.
Without it, a schema review can become a producer-only design meeting.
With it, the team can ask:
- Which fields are part of the published contract?
- Which fields are internal producer details that should not be published?
- Which consumers need the new shape?
- Which old consumers must survive during the transition?
- What signal would show that a "compatible" change was not compatible in practice?
This does not mean every consumer gets veto power over every producer change. It means the producer needs a compatibility policy that matches the promise it made by publishing the event.
Trade-offs and Limits
Compatibility improves independent deployment. It lets producers add behavior without coordinating every consumer release.
It costs time and contract discipline.
You may need to publish old and new fields together. You may need schema checks in CI. You may need consumer contract tests. You may need a deprecation window that lasts as long as your retention and replay policy.
Compatibility does not solve every event problem.
It does not guarantee that a consumer handles duplicates correctly. That was the outbox and inbox boundary from the previous lesson. It does not guarantee that replay is safe. Replay can re-expose old schemas to new code, which is why the next lesson treats replay and backfills as operational actions.
You can see the boundary when:
- Consumers parse the event but make wrong decisions.
- Old events in the log no longer match new consumer assumptions.
- A "minor" field change causes DLQ growth or lag.
- Analytics numbers shift after a schema change even though no parser failed.
The trade-off is direct:
More compatibility discipline buys safer independent deployment.
It also creates longer-lived contracts and slower removal of old shapes.
Common Confusions
Confusion: Versioning makes changes safe
Why it is tempting:
A schema_version field feels like a clear boundary between old and new events.
Better model:
Versioning helps consumers choose logic. It does not decide whether the meaning is safe. A version number without a migration rule is only a label.
Confusion: Adding fields is always safe
Why it is tempting:
Many formats allow readers to ignore unknown fields.
Better model:
Adding a field is safe only when old consumers can ignore it and the meaning of existing fields stays the same. A strict consumer, exhaustive enum match, or changed emission rule can still break.
Confusion: Schema compatibility is a broker concern
Why it is tempting:
Schema registries and broker tooling can enforce useful rules.
Better model:
Tooling can check many syntactic rules. Application teams still own semantic meaning, consumer promises, and release policy.
Check Your Understanding
Check: The order service wants to remove total_cents after publishing amount.value and amount.currency for two weeks. The event log keeps events for 90 days, and a reporting job can replay the last 60 days. What question should the team ask before removal?
Think first, then reveal.
Answer: They should ask whether every supported consumer, including replay jobs, has migrated away from total_cents for the full retention and replay window that matters. Two weeks of dual publishing may be too short if old events can still be replayed by code that expects the old field.
Practice
Review this proposed change.
Current event:
{
"event_type": "SubscriptionRenewed",
"subscription_id": "S-17",
"customer_id": "C-44",
"renewed_at": "2026-07-08T10:15:00Z",
"plan": "pro"
}
Proposed change:
{
"event_type": "SubscriptionRenewed",
"subscription_id": "S-17",
"account_id": "C-44",
"renewed_at": "2026-07-08T10:15:00Z",
"plan": {
"code": "pro",
"billing_period": "monthly"
}
}
Three consumers exist:
- Billing reads
subscription_id,customer_id, andplan. - Email reads
customer_idandrenewed_at. - Analytics reads
planfor conversion reports.
Classify the change and propose a safer migration path.
Model answer:
This is breaking if published as a one-step replacement. customer_id is renamed to account_id, and plan changes from a string to an object. Billing, email, and analytics may all fail or make wrong decisions.
A safer path is to publish both old and new fields for a transition:
{
"customer_id": "C-44",
"account_id": "C-44",
"plan": "pro",
"plan_details": {
"code": "pro",
"billing_period": "monthly"
}
}
Document the meaning of each field. Ask consumers to declare when they have migrated. Keep the old fields through the supported replay or retention window. Remove them only when the compatibility policy says old consumers and replay paths no longer depend on them.
Resources
- [BOOK] Martin Kleppmann - Designing Data-Intensive Applications
- Link: https://dataintensive.net/
- Focus: Read the chapters on data models, encoding, evolution, and logs as background for long-lived data contracts.
- [DOC] Confluent Schema Evolution
- Link: https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
- Focus: Use the compatibility modes as examples of syntactic compatibility policy.
- [REFERENCE] Apache Avro Specification
- Link: https://avro.apache.org/docs/current/specification/
- Focus: Look at schema resolution rules to see how readers and writers can evolve independently.
- [ARTICLE] Martin Fowler - Consumer-Driven Contracts
- Link: https://martinfowler.com/articles/consumerDrivenContracts.html
- Focus: Use the consumer perspective to think about event contracts, not only request-response APIs.
Key Takeaways
- Event schemas are compatibility promises between independently deployed systems.
- Syntactic compatibility means consumers can decode the event; semantic compatibility means they can still make the right decision.
- Safe evolution usually adds before it removes, keeps old meaning stable, and waits for consumer evidence before deleting old shapes.
- Schema tooling helps, but application teams still own meaning, release policy, replay windows, and operational signals.
← Back to Event-Driven Architecture and Streaming Foundations