Skip to content

Versioning Events

This guide explains how to evolve event types and structures in a safe, predictable, and maintainable way. It focuses on practical strategies for versioning events in EventSourcingDB and shows how to preserve compatibility without losing semantic clarity. You'll learn when to introduce new event types, how to handle schema changes, and what to avoid when working with immutable historical data.

In event-sourced systems, data is not updated — it is accumulated. Once an event has been written, it becomes part of the system's permanent history. That history must remain interpretable and meaningful, even as the software and business rules around it evolve. Versioning is the mechanism that makes this possible. It allows you to adapt your system to new needs without rewriting the past.

Events Are Immutable and Long-Lived

Because events in EventSourcingDB are immutable, they cannot be changed or deleted once written. This immutability ensures a reliable audit trail and enables safe, repeatable reprocessing — but it also introduces a constraint: you cannot simply "fix" a structural mistake or adjust an outdated payload.

Instead, your system must continue to support old event versions even as new ones are introduced. This creates the need for careful versioning, both in naming and in structure. Without a clear strategy, small changes can lead to large inconsistencies, broken projections, and ambiguous interpretations over time.

Use Type Names to Express Version Changes

EventSourcingDB does not include a dedicated version field in the event format. Instead, versioning is expressed through the type field. When a structural or semantic change is introduced that breaks compatibility, a new type should be created — typically by appending a version suffix to the name.

For example:

io.eventsourcingdb.library.book-acquired.v1
io.eventsourcingdb.library.book-acquired.v2

This approach makes the version explicit and allows different versions of the same conceptual event to coexist in the system. It also makes it easier to define clear schemas, write separate event handlers, and reason about the behavior of projections over time.

Avoid adding version numbers unless there is a breaking change. Not every refinement requires a new version. Only when previous consumers would fail — or misunderstand the event — should a new type be introduced.

What Counts as a Breaking Change?

A change is considered breaking if it violates the expectations of existing consumers or projections. Examples include:

  • Renaming or removing existing fields
  • Changing the type or semantics of a field
  • Making an optional field required
  • Introducing a new required field into a registered schema

These changes may prevent older code from correctly parsing or interpreting the event. They can lead to runtime errors, invalid projections, or silent misbehavior.

Non-breaking changes, on the other hand, are those that preserve the meaning and structure expected by existing consumers. These include:

  • Adding new optional fields
  • Extending enumerations or value sets
  • Refining internal structure without affecting existing fields
  • Changing default behavior in a way that does not alter output

Whenever in doubt, treat a change as breaking unless you are certain that all consumers can handle it safely.

Schema Registration Enforces Compatibility

EventSourcingDB allows you to register JSON schemas for each event type. Once registered, these schemas are strictly enforced. New events that do not match the schema are rejected. Existing events are also validated against the schema during registration — meaning you cannot retroactively impose constraints that older events violate.

This design ensures structural integrity but also increases the importance of versioning. If you need to add a required field or tighten validation, you cannot simply update the schema. Instead, you must define a new event type with a new schema and begin producing that version going forward.

This also affects projections and event handlers: they must be updated to recognize and process the new type separately. This might involve duplicating some logic, but it guarantees that no assumptions are broken silently.

Transforming Events Without Changing Them

Sometimes you may want to unify multiple event versions in a projection or read model. For example, both book-acquired.v1 and book-acquired.v2 might represent the same underlying concept, but with different field names or payload structures.

In such cases, it is recommended to handle transformation outside of the event store. You can use an adapter layer or transformation function in your projection code to normalize both versions into a common shape. This keeps the event log pristine while allowing your application to evolve and simplify its downstream logic.

EventSourcingDB also supports EventQL, which makes it possible to filter and transform data declaratively during queries. However, semantic normalization is best handled in application code, where business rules can be applied explicitly.

Designing for Compatibility From the Start

The need for versioning cannot be eliminated, but it can be reduced by thoughtful design. When modeling a new event type, consider:

  • Whether all fields are truly required, or whether some can be optional
  • Whether a field's name and type are likely to remain stable
  • Whether the semantics of the event are clear and unambiguous

Favor expressiveness over cleverness. Include all relevant domain information in the initial version, but avoid overfitting the structure to current implementation details. Think about how the event might be used, reinterpreted, or extended in the future.

Also consider how events relate to one another. If two types are tightly coupled, changes in one may require changes in the other. In such cases, version them together, and ensure projections are version-aware.

Accepting Divergence in the Event Log

It is perfectly acceptable — and often necessary — to have multiple versions of the same event type coexisting in the event store. Some streams may contain only old versions, others only new, and some may contain a mix. This reflects the natural evolution of a system over time.

Rather than trying to homogenize the log, embrace its historical accuracy. Each event tells the story of what was known, decided, and recorded at a particular moment. That fidelity is a strength, not a problem — as long as your code is equipped to interpret it correctly.