Refactoring in an event-sourced world – Upcasting in Axon 2
This article explains how the concept of upcasting can be applied to an event-sourced model. Focusing on demultiplexing of events, a new feature in Axon 2.
Introduction
All applications are subject to continuing change, meaning they have to “be continually adapted or they become progressively less satisfactory” [Lehman]. Refactoring is a common technique to prepare the program’s source code for newly incoming changes.
Refactoring in an event-sourced model is quite different from the more common object-relation model (ORM). In an event sourced model state is derived from events instead of being stored directly. To change the structure of an event means that a mechanism has to be provided to handle all events stored with the old structure. One of those mechanisms is called upcasting. This article will explain how upcasting can be used to facilitate refactoring in an event sourced model. We will use the Axon framework, a framework that helps applying the event-sourcing model, to explain the concepts of upcasting in detail. We’ll focus on demultiplexing of events, a new feature that I implemented for the upcoming Axon version (Axon 2).
Upcasting
Originally a concept of object-oriented programming, where: “a subclass gets casted to it’s superclass automatically when needed”, upcasting can also be applied to event sourcing. Upcasting in an event-sourced model allows for events to be refactored while keeping the complete event history of the model (all previous state) intact. Saving the application’s state this way brings several advantages (note that this list is not exhaustive):
- It can be used as an audit log, making problems easier to track down.
- All state changes are made explicit by name (e.g BirthNameDetailsUpdatedEvent), making it easy to understand what the program does.
- It allows you to keep track of historical changes, which might be relevant at a later stage in the development of your application. E.g. the client wants to perform some statistical analysis based on the usage of the application over time.
The main disadvantage of using an event-sourced model is when code needs to get refactored. Performing a big refactoring on your code base is hard because you’ll have to let your code work with all events that were fired by the old code base. To make refactoring possible when historical events do not fit your new model anymore is through upcasting.
Upcasters are manually written procedures that have one input event of revision x and output, typically one, new mutated event of revision x+1. Events already stored in the database prior to the moment of refactoring get transformed to the refactored version of that event through an upcaster. Upcasting in Axon is performed in a process before events are sent to the application, meaning that the rest of the application can be completely ignorant about outdated events.
Core to the process of upcasting in Axon is the UpcasterChain: a class containing all upcasters as specified by the caller in the order specified by the caller. The upcaster chain is responsible for upcasting incoming events and delegating each event to the upcasters in the specified order. The interface of the UpcasterChain looks like this:
public class UpcasterChain {
private List<Upcaster> chain;
public List<Event> upcast(Event event);
...
}
The UpcasterChain combines upcasters so that one upcaster only needs to know about one event type of one specific revision. This allows the programmer to write upcasters that are small, isolated, and easy to understand.
Upcasting and demultiplexing
To demultiplex data is to take data from a single input channel and divide that data over multiple output channels. When we apply this concept to upcasting this means that one event can be transformed into multiple events through an upcaster.
Each upcaster indicates whether it can process the type of the event that needs upcasting. If the upcaster’s type matches with the type of the incoming event the chain will pass the event to the upcaster. The upcaster will then process the event and return zero, one or multiple ‘upcasted’ events. Since upcasters are arranged in a chain all events coming from the upcaster are passed into all remaining upcasters that are still left in the chain, as shown in the following pseudo code:
method upcast(chain, event):
if chain is empty then
no upcasting needs to be done return a singleton list
with the given event
else
upcast the event with the first upcaster in the list, like this:
processed events = chain.head.upcast(event)
process each upcasted event through all remaining upcasters
in the chain (chain.tail) by recursively calling the upcast
method on all these events and return the result as a list.
this list will represents all fully upcasted events.
To illustrate how demultiplexing works in practice we’ll explain a simple case of updating administrative details for a user:
It so happens to be that there is a system running in production containing many AdministrativeDetailsUpdated events. New requirements have let you to introduce two new events: AddressUpdatedEvent and InsuranceDetailsUpdatedEvent which are processed by different parts of the application. Previously though, all information in these two events was contained in the old AdministrativeDetailsUpdatedEvent, which is now deprecated. In Axon 1 it is not possible to handle the situation described in the example properly because upcasters cannot do demultiplexing. In other words, the granularity of old events cannot be changed through upcasting so there is no way of nicely separating the business logic of handling address changes and insurance details changes. Ultimately this will either make programmers reluctant in refactoring their model or it will make them introduce hacks to let it work, degrading the readability of the source code. In Axon 2 an upcaster can be implemented to transform these events into instances of AddressUpdatedEvent and InsuranceDetailsUpdatedEvent and pass them to the application code, solving the previously described problem.
Conclusion
We’ve introduced the concept of upcasting in an event-sourced application and we’ve shown how this concept is useful by providing a real world example. We’ve shown that upcasting in Axon 2 is more flexible from its Axon 1 counterpart due to its ability to demultiplex events.
References
[Lehman] Lehman, M. M. (1980). “On Understanding Laws, Evolution, and Conservation in the Large-Program Life Cycle”. Journal of Systems and Software