Event Transformation
When an event’s stored structure no longer matches what your handlers and projectors expect, an event transformation rewrites it on the way out of the event store. The stored events stay exactly as they were written. The transformation runs every time those events are read, handing each handler the shape it expects.
A transformation is the tool for the structural changes that payload conversion cannot express on its own: combining two fields into one, wrapping values in a new object, lifting a historic event onto a new version, or renaming an event whose identity changed.
When to reach for a transformation
The Converter absorbs most schema evolution on its own, in two ways:
-
Payload conversion at handling time: each handler declares the type it wants, and the converter produces it from the stored payload.
-
Converter-level compatibility settings: defaults, ignore-unknown rules, and format-specific mappings (for example Jackson annotations, Avro reader and writer schemas, or Jakarta XML Binding), configured on the converter rather than on the transformation chain.
Whether a change needs a transformation therefore depends as much on how your converter is configured as on the change itself: a lenient configuration absorbs more, a strict one rejects more. Adding fields with defaults, dropping fields a handler no longer reads, renaming fields, and changing compatible field types are all typically handled here.
Configure your converter first, and reach for a transformation only when the change cannot be expressed there, for example:
-
You need to restructure the payload itself: combine
firstNameandlastNameinto a singlefullName, or wrapminCapacityandmaxCapacityinto arangeobject. -
You want one rule to apply to every handler of an event, rather than adjusting each handler’s type or annotations individually.
-
You are lifting historic events onto a new version so the rest of your application only ever deals with the current shape.
|
Conversion or transformation?
A converter changes an event’s representation, while a transformation changes its structure. See Event versioning for the conversion-first approach. |
What a transformation looks like
A single transformation looks like this:
EventTransformation transformation =
EventTransformation.from(new MessageType("coursecatalog.CoursePublished", "1.0.0")) (1)
.to(new MessageType("coursecatalog.CoursePublished", "2.0.0")) (2)
.transform(JsonNode.class, CoursePublishedV1ToV2::map); (3)
| 1 | from(…) selects the stored event identity to match. |
| 2 | to(…) declares the identity the transformation produces: same name, new version. |
| 3 | transform(…) maps the old payload shape to the new one. |
You register one transformation per version step and let Axon Framework apply them in order, so a v1 event read today reaches your handler as the current version.
A transformation maps the payload either as an intermediate representation, such as JsonNode, Map, or raw bytes, or as a typed record that mirrors the stored shape.
The structural form needs no extra classes, the typed form gives compile-time safety, and you can mix the two across a chain.
From here:
-
Defining Transformations shows how to write the individual transformations, map straight to a typed event, work with generic payloads, match several event versions, rename an event, and drop one entirely.
-
Configuring Transformations shows how to assemble them into a chain, register it, and test it.
-
Course Catalog Example is a runnable example that exercises every technique on a course catalog end to end.
Adding the module
Event transformation ships in the axoniq-message-transformation module:
<dependency>
<groupId>io.axoniq.framework</groupId>
<artifactId>axoniq-message-transformation</artifactId>
<version>${axoniq.version}</version>
</dependency>
Once the module is on the classpath and an EventTransformerChain is registered, Axon Framework applies it automatically whenever events are read from the store.