| consumer | ||
| producer | ||
| .gitignore | ||
| pom.xml | ||
| README.md | ||
| settings.xml | ||
forward-compatible-events
The old consumer must survive the new producer — not the other way around.
A minimal demo of forward-compatible event schemas: a consumer app, written
and deployed knowing only one version of an event, kept working while a
producer app evolves that event three times — including inventing a brand
new event type the consumer has never heard of.
Backward compatibility (new code reads old data) is the intuitive half of schema evolution. Forward compatibility (old code survives new data) is the half that actually matters for event-driven systems where you don't control when every consumer redeploys — which is exactly the constraint this demo is built around.
Modules
| Module | What it is | Runs? |
|---|---|---|
producer |
Publishes order events. Ships four JSON fixtures simulating four moments in the event's life, and POSTs whichever one you ask for to the consumer. | port 8081 |
consumer |
Receives order events. Coded once, against v1 only (orderId, amount) — never updated to know about what producer does later. |
port 8080 |
There is no shared contract module between them, on purpose. In real life
the producer team ships changes the consumer team never compiles against; a
shared Java DTO would hide exactly the risk this demo is about. The JSON wire
format is the only thing the two sides agree on.
┌──────────┐ POST /events (raw JSON) ┌──────────┐
│ producer │ ───────────────────────────▶│ consumer │
│ :8081 │ │ :8080 │
└──────────┘ └──────────┘
The four scenarios
| Scenario | What changes | Consumer's reaction |
|---|---|---|
v1 |
Baseline: { orderId, amount } |
Processed — this is what it was built for |
v2-additive-field |
New optional field in data (currency) and a new top-level field (producedBy) |
Processed — extra fields are simply never read |
v3-new-event-type |
A brand new eventType (ORDER_REFUNDED) the consumer's code has never seen |
Logged and recorded as ignored — not a crash |
v4-breaking-rename |
orderId renamed to id inside data |
Rejected with 422 and a clear message — this is a real breaking change, and forward compatibility was never supposed to survive it |
The first three prove genuine forward compatibility. The fourth is there deliberately, to draw the line: forward compatibility covers additive changes and new types, not renaming or removing a field a consumer actually depends on.
What actually makes the consumer forward-compatible
Three small, specific choices in the consumer module — see
EventEnvelope,
EventType and
EventProcessingService:
@JsonIgnoreProperties(ignoreUnknown = true)on the envelope — an unexpected top-level field (producedByinv2) doesn't fail deserialization.datastays a genericMap<String, Object>, never a typed class per event version — handlers read only the keys they need and never look at the rest.- Unknown enum values decode to a sentinel, not an exception — an
UNKNOWNconstant annotated@JsonEnumDefaultValue, combined withspring.jackson.deserialization.read-unknown-enum-values-using-default-value: true, turns a type the consumer has never heard of into "safely ignored today" instead of a500.
None of this is exotic — it's mostly turning off Jackson's defaults, which are strict by default. That's also why it's easy to get wrong without deciding to: the strict behavior is what you get for free if you don't think about it.
Requirements
- Java 21+
- Maven 3.8+
Maven repositories. The commands below pass
-s settings.xml, a checked-in settings file that resolves everything from Maven Central. Drop it if your default Maven setup already reaches Central.
Quick start
Build & run the tests
mvn -s settings.xml clean install
The interesting part is in
EventProcessingForwardCompatibilityTest:
four tests, one per scenario above, posting hand-written JSON (not built from
a shared class — see above) straight at the consumer.
See it live
java -jar consumer/target/consumer-1.0.0-SNAPSHOT.jar &
java -jar producer/target/producer-1.0.0-SNAPSHOT.jar &
curl -X POST http://localhost:8081/publish/v1
curl -X POST http://localhost:8081/publish/v2-additive-field
curl -X POST http://localhost:8081/publish/v3-new-event-type
curl -X POST http://localhost:8081/publish/v4-breaking-rename # → 422, on purpose
curl http://localhost:8080/events # the consumer's view of everything it received
Technologies
- Spring Boot 3.3.4 · Java 21 · Jackson
License
Apache 2.0