Configuring Transformations
Individual transformations are assembled into an EventTransformerChain and registered once with your configuration.
Axon Framework then applies the chain whenever events are read from the store.
Registering the chain
Build the chain with the transformations in the order they should run:
import io.axoniq.framework.messaging.transformation.events.EventTransformerChain;
public final class CourseCatalogTransformations {
public static EventTransformerChain chain() {
return EventTransformerChain.builder() (1)
.register(SystemAnnouncementLegacyUplift.build())
.register(CoursePublishedV1ToV2.build()) (2)
.register(CoursePublishedV2ToV3.build())
.register(StudentRegisteredV1ToV2.build())
.register(WelcomeMessageBetaCleanup.build())
.register(SystemHeartbeatDrop.build()) (3)
.build(); (4)
}
}
| 1 | Start building the chain. |
| 2 | Each transformation runs in the order it is registered. See Why ordering matters. |
| 3 | A drop is registered like any other transformation; it removes its matching event from the read stream. |
| 4 | build() produces an immutable chain, ready to register as a component. |
Then make that chain available to your configuration as an EventTransformerChain component.
You only ever supply the chain. The framework’s configuration detects the chain and decorates the event store with transformation logic for you. Hence, there is nothing to wire on the event store itself for users.
How you register the component depends on your setup:
-
Declarative - Configuration API
-
Autodetected - Spring Boot
Register the chain as a component on your EventSourcingConfigurer:
configurer.componentRegistry(registry -> registry
.registerComponent(EventTransformerChain.class, (1)
config -> CourseCatalogTransformations.chain()));
| 1 | Register the chain under EventTransformerChain.class. Axon Framework’s transformation enhancer detects this component and decorates the event store with it. |
Expose the chain as a bean of type EventTransformerChain.
Axon’s Spring Boot integration registers it as a component, where the same enhancer detects it:
@Bean
public EventTransformerChain eventTransformerChain() {
return CourseCatalogTransformations.chain();
}
Both approaches do the same thing: they provide one EventTransformerChain component, and the event store then applies it on every read.
Register no chain and the event store is left untouched.
The chain is built once at startup and is immutable afterwards. The course catalog example assembles a fuller chain the same way, adding a rename and more version hops alongside the drop shown here.
|
Transformation runs before interceptors
The enhancer installs the transformation so that, on every read, events are transformed to their current shape before any message interceptor or handler sees them. Interceptors and handlers therefore never deal with outdated event shapes. If you instead need an interceptor to observe the original, stored shape, replace the default transformation enhancer with your own that installs the transformation at a different order. |
Why ordering matters
A stored event flows through the chain, with each matching transformation’s output feeding the next, until no further transformation matches. Register transformations in version order so each hop hands off to the one after it:
.register(CoursePublishedV1ToV2.build()) // 1.0.0 -> 2.0.0
.register(CoursePublishedV2ToV3.build()) // 2.0.0 -> 3.0.0
A stored v1 CoursePublished is lifted to v2 by the first transformation, then that result is lifted to v3 by the second, so your handler always receives the current shape.
|
Prefer small, single-step transformations
Write one transformation per version step ( |
|
Safety bound
The chain stops re-applying transformations to an event after a fixed number of hops (100 by default) to guard against accidental cycles.
If a single event legitimately needs more hops, raise the bound with |
When more than one transformation matches
Chaining handles the common case where each transformation matches a different identity and hands off to the next.
When several registered transformations could match the same stored event, the chain resolves it like Java overload resolution: the most specific match wins.
An exact identity match (a concrete from) always wins over a predicate from, independent of registration order.
A predicate runs only when no exact match claims the event; when several predicates overlap, the first registered one wins.
Suppose every 0.x beta WelcomeMessageSent folds up to 1.0.0, but the 0.9.0 beta needs its own mapping:
QualifiedName welcomeMessageSent = new QualifiedName("coursecatalog.WelcomeMessageSent");
MessageType v1 = new MessageType(welcomeMessageSent, "1.0.0");
EventTransformation betaCleanup = (1)
EventTransformation.from(type -> type.version().startsWith("0."))
.declaringFromTypes(welcomeMessageSent)
.to(v1)
.transform(JsonNode.class, WelcomeMessageBetaCleanup::map);
EventTransformation beta090ToV1 = (2)
EventTransformation.from(new MessageType(welcomeMessageSent, "0.9.0"))
.to(v1)
.transform(JsonNode.class, WelcomeMessage090ToV1::map);
EventTransformerChain.builder()
.register(betaCleanup)
.register(beta090ToV1)
.build();
| 1 | A broad predicate matching every 0.x version (0.x → 1.0.0) with from(Predicate), scoped to WelcomeMessageSent via declaringFromTypes(…). |
| 2 | An exact transformation for just the 0.9.0 version (0.9.0 → 1.0.0) with from(MessageType), which therefore wins for a stored 0.9.0 event. |
A stored 0.9.0 event matches both.
The exact 0.9.0 transformation wins, producing 1.0.0, which no transformation matches further, so the predicate never runs for 0.9.0 while still handling every other 0.x version.
Registering the two in the other order changes nothing: the exact match always wins.
|
What the chain validates, and when
Two transformations that match the same exact identity are a configuration error, reported as a |
Writing safe mappers
A mapper is invoked many times: once per matching event, every time those events are read. Keep it a pure function:
-
Deterministic: the same input always produces the same output. No clocks, randomness, counters, or calls to external services.
-
Side-effect-free and thread-safe: no mutable shared state, and do not publish messages, write to a database, or mutate the input.
These rules keep replays reproducible and let the chain run while events stream concurrently.
Testing a transformation
Because a transformation is a deterministic function from one payload to another, you can verify it in isolation, with no event store and no running application. Build the transformation, feed it a sample stored payload, and assert on the transformed output.
The course catalog example does this with a small given/when/then harness, TransformationTester, that builds a one-transformation chain and runs the sample event through it:
TransformationTester.forTransformation(CoursePublishedV1ToV2.build()) (1)
.given()
.messageType(COURSE_PUBLISHED, "1.0.0") (2)
.payloadFromResource("/transformations/coursepublished/v1.json")
.when()
.then()
.success()
.outputType(new MessageType(COURSE_PUBLISHED, "2.0.0")) (3)
.outputPayloadFromResource("/transformations/coursepublished/v2.json");
| 1 | The transformation under test, built from its from/to/transform definition. |
| 2 | The stored event to feed in: qualified name, version, and payload. |
| 3 | Assert on the output: its new type and payload. |
TransformationTester is a few lines of test code in the example, not a framework dependency.
AxonIQ plans to provide integrated transformation testing in a future release. For now, copy TransformationTester into your own project, or assert on the built transformation however you prefer.
|
Untyped output is not identity-checked
When a mapper returns a typed object, such as the current event from a typed mapper, Axon Framework resolves its identity and raises a |
To verify the handlers and projectors that consume transformed events end to end, see Testing.