Versioning Events

In the lifecycle of an Axon application events will typically change their format. As events are stored indefinitely the application should be able to cope with several versions of an event. This chapter will discuss what to keep in mind when creating your events, for backwards (and forwards) compatibility, and it will explain the upcasting process.

Event Upcasting

Due to the ever-changing nature of software applications it is likely that event definitions also change over time. Since the Event Store is considered a read and append-only data source, your application must be able to read all events, regardless of when they have been added. This is where upcasting comes in.

Originally a concept of object-oriented programming, where "a subclass gets cast to its superclass automatically when needed", the concept of upcasting can also be applied to event sourcing. To upcast an event means to transform it from its original structure to its new structure. Unlike OOP upcasting, event upcasting cannot be done in full automation because the structure of the new event is unknown to the old event. Manually written Upcasters have to be provided to specify how to upcast the old structure to the new structure.

Upcasters are classes that take one input event of revision x and output zero or more new events of revision x + 1. Moreover, upcasters are processed in a chain, meaning that the output of one upcaster is sent to the input of the next. This allows you to update events in an incremental manner, writing an Upcaster for each new event revision, making them small, isolated, and easy to understand.

Note

Perhaps the greatest benefit of upcasting is that it allows you to do non-destructive refactoring, i.e. the complete event history remains intact.

In this section we'll explain how to write an upcaster, describe the different (abstract) implementations of the Upcaster that come with Axon, and explain how the serialized representations of events affects how upcasters are written.

To allow an upcaster to see what version of serialized object they are receiving, the Event Store stores a revision number as well as the fully qualified name of the Event. This revision number is generated by a RevisionResolver, configured in the serializer. Axon provides several implementations of the RevisionResolver, such as the AnnotationRevisionResolver, which checks for an @Revision annotation on the Event payload, a SerialVersionUIDRevisionResolver that uses the serialVersionUID as defined by Java Serialization API and a FixedValueRevisionResolver, which always returns a predefined value. The latter is useful when injecting the current application version. This will allow you to see which version of the application generated a specific event.

Maven users can use the MavenArtifactRevisionResolver to automatically use the project version. It is initialized using the groupId and artifactId of the project to obtain the version for. Since this only works in JAR files created by Maven, the version cannot always be resolved by an IDE. If a version cannot be resolved, null is returned.

Axon's upcasters do not work with the EventMessage directly, but with an IntermediateEventRepresentation. The IntermediateEventRepresentation provides functionality to retrieve all necessary fields to construct an EventMessage (and thus a upcasted EventMessage too), together with the actual upcast functions. These upcast functions by default only allow the adjustment of the events payload, payload type and additions to the event its metadata. The actual representation of the events in the upcast function may vary based on the event serializer used or the desired form to work with, so the upcast function of the IntermediateEventRepresentation allows the selection of the expected representation type. The other fields, for example the message/aggregate identifier, aggregate type, timestamp etc. are not adjustable by the IntermediateEventRepresentation. Adjusting those fields is not the intended work for an Upcaster, hence those options are not provided by the provided IntermediateEventRepresentation implementations.

The basic Upcaster interface for events in the Axon Framework works on a Stream of IntermediateEventRepresentations and returns a Stream of IntermediateEventRepresentations. The upcasting process thus does not directly return the end result of the introduced upcast functions, but chains every upcasting function from one revision to another together by stacking IntermediateEventRepresentations. Once this process has taken place and the end result is pulled from them, that is when the actual upcasting function is performed on the serialized event.

Provided abstract Upcaster implementations

As described earlier, the Upcaster interface does not upcast a single event; it requires a Stream<IntermediateEventRepresentation> and returns one. However, an upcaster is usually written to adjust a single event out of this stream. More elaborate upcasting set ups are also imaginable, for example from one events to multiple, or an upcaster which pulls state from an earlier event and pushes it in a later one. This section describes the currently provided abstract implementations of event upcasters which a user can extend to add its own desired upcast functionality.

  • SingleEventUpcaster - a one-to-one implementation of an event upcaster.

    Extending from this implementation requires one to implement a canUpcast and doUpcast function, which respectively check whether the event at hand is to be upcasted, and if so how it should be upcasted.

    This is most likely the implementation to extend from, as most event adjustments are based on self contained data and are one to one.

  • EventMultiUpcaster - a one-to-many implementation of an event upcaster.

    It is mostly identical to a SingleEventUpcaster, with the exception that the doUpcast function returns a Stream instead of a single IntermediateEventRepresentation.

    As such this upcaster allows you to revert a single event to several events.

    This might be useful if you for example have figured out you want more fine grained events from a fat event.

  • ContextAwareSingleEventUpcaster - a one-to-one implementation of an Upcaster, which can store context of events during the process.

    Next to the canUpcast and doUpcast, the context aware Upcaster requires one to implement a buildContext function, which is used to instantiate a context which is carried between events going through the upcaster.

    The canUpcast and doUpcast functions receive the context as a second parameter, next to the IntermediateEventRepresentation.

    The context can then be used within the upcasting process to pull fields from earlier events and populate other events.

    It thus allows you to move a field from one event to a completely different event.

  • ContextAwareEventMultiUpcaster - a one-to-many implementation of an upcaster, which can store context of events during the process.

    This abstract implementation is a combination of the EventMultiUpcaster and ContextAwareSingleEventUpcaster, and thus services the goal of keeping context of IntermediateEventRepresentations and upcasting one such representation to several.

    This implementation is useful if you not only want to copy a field from one event to another, but have the requirement to generate several new events in the process.

Writing an Upcaster

The following Java snippets will serve as a basic example of a one to one upcaster (the SingleEventUpcaster).

Old version of the event:

@Revision("1.0")
public class ComplaintEvent {
private String id;
private String companyName;
// Constructor, getter, setter...
}

New version of the event:

@Revision("2.0")
public class ComplaintEvent {
private String id;
private String companyName;
private String description; // New field
// Constructor, getter, setter...
}

Upcaster from 1.0 revision to 2.0 revision:

// Upcaster from 1.0 revision to 2.0 revision
public class ComplaintEventUpcaster extends SingleEventUpcaster {
private static SimpleSerializedType targetType =
new SimpleSerializedType(ComplainEvent.class.getTypeName(), "1.0");
@Override
protected boolean canUpcast(IntermediateEventRepresentation
intermediateRepresentation) {
return intermediateRepresentation.getType().equals(targetType);
}
@Override
protected IntermediateEventRepresentation doUpcast(
IntermediateEventRepresentation intermediateRepresentation) {
return intermediateRepresentation.upcastPayload(
new SimpleSerializedType(targetType.getName(), "2.0"),
org.dom4j.Document.class,
document -> {
document.getRootElement()
.addElement("description")
.setText("no complaint description"); // Default value
return document;
}
);
}
}

Spring boot configuration:

@Configuration
public class AxonConfiguration {
@Bean
public SingleEventUpcaster myUpcaster() {
return new ComplaintEventUpcaster();
}
@Bean
public JpaEventStorageEngine eventStorageEngine(Serializer eventSerializer,
Serializer snapshotSerializer,
DataSource dataSource,
SingleEventUpcaster myUpcaster,
EntityManagerProvider entityManagerProvider,
TransactionManager transactionManager) throws SQLException {
return JpaEventStorageEngine.builder()
.eventSerializer(eventSerializer)
.snapshotSerializer(snapshotSerializer)
.dataSource(dataSource)
.entityManagerProvider(entityManagerProvider)
.transactionManager(transactionManager)
.upcasterChain(myUpcaster)
.build();
}
}