Managing Event Schema and Versioning
1. Event Schema Evolution Pattern
| Change | Compatible? | Strategy |
| Add optional field | Backward compatible | Default value for old consumers |
| Remove field | Forward compatible only | Keep field, mark deprecated |
| Rename field | Breaking | Add new, keep old, dual-write |
| Change type | Breaking | New event version |
| Add required field | Breaking | Must be optional or new version |
2. Event Versioning Pattern
| Approach | Detail |
| Field-Level Versioning | Add new fields without breaking old consumers |
| Schema Versioning | Embed schemaVersion in payload |
| Topic Versioning | New topic name (orders.v2) for breaking changes |
| Type-Embedded | Event type carries version: OrderPlaced.v2 |
3. Schema Registry Pattern
| Feature | Detail |
| Centralized Catalog | All schemas (Avro/Protobuf/JSON Schema) stored centrally |
| Compatibility Check | Enforce backward/forward/full compatibility on register |
| Schema ID in Message | Producer prefixes schema ID; consumer fetches schema by ID |
| Tools | Confluent Schema Registry, Apicurio, AWS Glue Schema Registry |
4. Tolerant Reader Pattern
| Principle | Detail |
| Ignore Unknown Fields | Don't fail on extra fields |
| Optional Defaults | Treat missing fields as default |
| Robustness | Survive producer schema additions without redeploy |
Example: Jackson Tolerant Reader
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OrderEvent evt = mapper.readValue(json, OrderEvent.class);
5. Expand and Contract Pattern
| Phase | Action |
| 1. Expand | Producer emits both old and new fields/formats |
| 2. Migrate | Consumers updated to read new format |
| 3. Contract | Producer drops old format once all consumers migrated |
6. Upcaster Pattern
| Aspect | Detail |
| Purpose | Transform old event versions to current on read |
| Where | Inside event store / consumer deserializer |
| Benefit | Domain code only handles latest schema |
| Tools | Axon Upcaster, custom in deserializer |
Example: Upcaster
// v1: { "amount": 100 } → v2: { "amount": 100, "currency": "USD" }
public class OrderPlacedV1ToV2 implements Upcaster {
public JsonNode upcast(JsonNode v1) {
((ObjectNode) v1).put("currency", "USD");
return v1;
}
}
7. Event Wrapper Pattern
| Layer | Contains |
| Wrapper (metadata) | id, type, version, occurredAt, correlationId, source |
| Payload | Domain-specific event body |
| Benefit | Generic processing (routing, dedupe, tracing) by metadata |
8. Canonical Data Model Pattern
| Aspect | Detail |
| Definition | Single org-wide model for shared concepts (Customer, Product) |
| Pros | Reduces N×N translations; consistent vocabulary |
| Cons | Becomes lowest-common-denominator; high coordination cost |
| Modern Take | Prefer per-context models + ACLs; canonical only for inter-org |
Warning: Org-wide canonical models often become bottlenecks. Use sparingly and only for truly universal concepts.
9. Event Envelope Pattern
| Field | Purpose |
| eventId | Unique ID for dedup |
| eventType | Discriminator (e.g., OrderPlaced) |
| eventVersion | Schema version |
| aggregateId | Subject of event |
| occurredAt | Timestamp |
| correlationId | Request trace ID |
| causationId | ID of event that caused this one |
| payload | Domain data |
Example: CloudEvents Envelope
{
"specversion": "1.0",
"id": "evt_8af1",
"type": "com.shop.order.placed.v2",
"source": "/order-service",
"time": "2026-05-15T10:00:00Z",
"subject": "order/ord_123",
"datacontenttype": "application/json",
"data": { "orderId": "ord_123", "total": 99.50, "currency": "USD" }
}
10. Schema Validation Pattern
| Stage | Validation |
| Producer-Side | Validate before publish; fail fast in tests/CI |
| Broker-Side | Schema registry rejects incompatible |
| Consumer-Side | Validate on consume; reject malformed → DLQ |
| Contract Tests | Pact/CDC tests verify producer-consumer compatibility |