No description
Find a file
2026-07-17 15:22:22 +02:00
consumer create forward compatible examples 2026-07-17 13:50:22 +02:00
producer fix events examples 2026-07-17 15:22:22 +02:00
.gitignore create forward compatible examples 2026-07-17 13:50:22 +02:00
pom.xml create forward compatible examples 2026-07-17 13:50:22 +02:00
README.md create forward compatible examples 2026-07-17 13:50:22 +02:00
settings.xml create forward compatible examples 2026-07-17 13:50:22 +02:00

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:

  1. @JsonIgnoreProperties(ignoreUnknown = true) on the envelope — an unexpected top-level field (producedBy in v2) doesn't fail deserialization.
  2. data stays a generic Map<String, Object>, never a typed class per event version — handlers read only the keys they need and never look at the rest.
  3. Unknown enum values decode to a sentinel, not an exception — an UNKNOWN constant annotated @JsonEnumDefaultValue, combined with spring.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 a 500.

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