Event Upcasting
Events are immutable by definition, since they do not describe the status quo, but something that happened in the past and thus can no longer be changed.
This accounts for all the information associated with an event, that is:
- its application-specific payload
- any application-specific meta-data
- any meta-data originating from the event store, for instance assigned id, time, etc.
Software, in contrast to that, is flexible and will be subject to change, sooner or later. This may affect the classes and data structures representing the events in-memory, since most likely no application will operate directly on the raw event data. Such changes typically include:
Change | Example |
---|---|
structural | renamed attributes |
data type | conversion from number to string |
semantics | changed interpretation of content/values |
granularity | two separate events formerly expressed as one (or vice versa) |
Mutable data stores
While the immutability of events enforces us to deal with these changes, they are not restricted to event-sourced applications. Applications using mutable data stores, such as SQL databases, also have to update their storage schema or migrate the data stored therein, if necessary.
Versioning
As events change or evolve over time, it is essential to know which version of the event
needs to be mapped to which class representation. While one could introduce a new class for
every possible change, this would result in a massive maintenance overhead within the remaining
code base interacting with those events. More likely, events sharing a common semantic, such as
a BookPurchasedEvent
should share the same class, even if the event changed over time.
The following diagram shows a scenario, where the raw events A and B evolved over
time (into compatible A', A'', and B'), still they should be mapped to the same Java class, when loaded:
block-beta
columns 1
block:stream
1["A"] space 2["B"] space 3["A'"] space 4["B"] space 5["C"] space 6["B'"] space 7["A''"]
end
1 --> 2
2 --> 3
3 --> 4
4 --> 5
5 --> 6
6 --> 7
space:2
block:events
BookPurchasedEvent space BookLentEvent space BookReturnedEvent
end
1 --> BookPurchasedEvent
3 --> BookPurchasedEvent
7 --> BookPurchasedEvent
2 --> BookLentEvent
4 --> BookLentEvent
6 --> BookLentEvent
5 --> BookReturnedEvent
classDef classA fill:olive;
class 1,3,7,BookPurchasedEvent classA
classDef classB fill:navy;
class 2,4,6,BookLentEvent classB
classDef classC fill:sienna;
class 5,BookReturnedEvent classC
Events A, A', and A'', or B and B' respectively, can be considered different versions of the same event type. These should be stored within the event store upon publishing new events for future read operations.
Type vs. Version
An event store does not necessarily have to support both - type and version - for
storing events. It is sufficient to encode an event version as part of the type identifier,
for instance: com.opencqrs.library.book.purchased.v1
. Reducing the evolutionary nature
of events to a simple type identifier also bears the advantage, that the event store
does not have to take care of event compatibility, as it considers each new version a
different type.
Upcasting
With the aforementioned type information stored as part of the raw events, it is possible to migrate them to their newest version, before actually mapping them to the Java classes. This process is called upcasting.
The so-called upcasters are programmed to transform raw events from any older version or type to a newer one. They are executed in an ad-hoc fashion, whenever an event needs to be migrated, while the result of the transformation is never persisted. Accordingly, the list of required upcasters is compounding over time, as the system evolves.
Moreover, upcasters are considered simple functions, transforming a single event into its new representation (which may be that of two separate events as well). Accordingly, they are usually chained to reduce the permutational complexity of having to maintain upcasters for each combination of old and new version. The following diagram shows how such an upcaster chain integrates with the process of loading events from the event store:
block-beta
columns 1
block:stream
1["A"] space 2["B"] space 3["A'"] space 4["B"] space 5["C"] space 6["B'"] space 7["A''"]
end
1 --> 2
2 --> 3
3 --> 4
4 --> 5
5 --> 6
6 --> 7
space:2
block:upcasters
U1["A to A'"] space U2["A' to A''"] space U3["B to B'"]
end
space:2
block:events
BookPurchasedEvent space BookLentEvent space BookReturnedEvent
end
1 --> U1
U1 --> U2
U2 --> BookPurchasedEvent
3 --> U2
7 --> BookPurchasedEvent
2 --> U3
U3 --> BookLentEvent
4 --> U3
6 --> BookLentEvent
5 --> BookReturnedEvent
classDef classA fill:olive;
class 1,3,7,U1,U2,BookPurchasedEvent classA
classDef classB fill:navy;
class 2,4,6,U3,BookLentEvent classB
classDef classC fill:sienna;
class 5,BookReturnedEvent classC
With that mechanism in place, it is possible to evolve events over time, still preserving the immutability of an event store.