← Recipes

阅读中文版本

Add Event Schema Versioning with an Envelope

Wrap events in a versioned envelope, keep payload changes compatible, and upcast old versions when reading so historical events remain replayable.

~35 mineventsdata-integrationcontracts

Task

Evolve event payloads without breaking existing consumers or rewriting historical events.

When to use this

A producer adds a required field to an event. Older consumers fail to deserialize, and replaying old events produces inconsistent state.

Prerequisites

  • An event bus or outbox that preserves the event payload
  • A consumer layer that can branch on schema version
  • A test corpus of historical event payloads

Steps

1. Add a stable envelope with event id, type, version, timestamp, and payload.

type EventEnvelope<T> = {
  id: string;
  type: string;
  version: number;
  occurredAt: string;
  payload: T;
};

Expected output: Every event carries an explicit version and identity.

2. Make additive optional changes in a new version and upcast old events when reading.

function upcast(event: EventEnvelope<unknown>): EventEnvelope<CurrentOrder> {
  if (event.version === 1) {
    return { ...event, version: 2, payload: { ...event.payload, currency: 'USD' } };
  }
  return event as EventEnvelope<CurrentOrder>;
}

Expected output: A version 1 event is read without rewriting the stored record.

3. Replay a corpus of old and new events and assert the same business outcome.

node --test event-replay.test.mjs

Expected output: Consumers handle every version in the corpus.

Troubleshooting

Problem

Consumers see an unknown version

Solution

Deploy consumers that can handle it before producers emit it, or keep the producer on the old version until migration is complete.

Problem

Upcasting changes business meaning

Solution

Upcast only representation, not semantics; a changed meaning needs a new event type.

Limitations

  • The envelope does not enforce compatibility; a schema registry or tests must do that.
  • Upcasters add a code path for every old version; remove them only when old events are no longer retained.

Version the envelope, not the topic

An event envelope carries the metadata needed to consume the payload safely: a unique id, type, schema version, and timestamp. The topic can stay stable while the payload evolves. Consumers branch on version and use an upcaster to produce the current internal shape.

{
  "id": "evt-42",
  "type": "OrderCreated",
  "version": 2,
  "occurredAt": "2026-10-20T10:00:00Z",
  "payload": { "orderId": "o-1", "total": 4200, "currency": "USD" }
}

Compatibility is a deployment order

A new consumer should understand the old version before producers emit the new one. A new producer should not emit a version that older consumers cannot ignore safely. Additive optional fields are easiest; required fields, renames, and type changes need a transition or a new event type.

Test historical events

Keep a corpus of real old payloads and replay them through the current consumer. Assert the business outcome, not only deserialization. A replay test catches upcaster mistakes that unit tests with constructed payloads miss.