OpenRewrite Code Conversion
Automated code conversion gives you a running head start on the upgrade by mechanically rewriting the parts of your Axon Framework 4 codebase that follow a predictable pattern. It handles the bulk of the rename and reshape work—package moves, annotation rewrites, accessor renames, and more—so you can focus your attention on the design decisions that genuinely require human judgment.
|
Code conversion is a starting point, not a finishing line.
After the recipes run, your project will not compile.
That is intentional.
The compile errors that remain, together with the |
Code conversion covers only source and build file transformations. It does not modify event store data, token tables, dead-letter queue entries, saga state, or any other runtime or database content. The recipes have been validated on Java, Spring Boot, and Maven projects, but should work with Kotlin and Gradle as well.
For the full list of available invocation options, see the migration module README in the Axon Framework repository.
Choosing a recipe
Two top-level recipes are available depending on whether you are targeting the free Axon Framework or the commercial Axoniq Framework:
| Recipe | Description | When to use |
|---|---|---|
|
Upgrades an Axon Framework 4 application to Axon Framework 5.
Renames packages and classes within the |
You are targeting free Axon Framework 5 and your AF4 application does not use Axon Server connector, the sequenced dead-letter queue, |
|
Composes the free recipe, then layers commercial upgrades on top and swaps the Spring Boot starter and BOM to the commercial coordinates. |
Recommended for any AF4 application that uses Axon Server, the DLQ, distributed messaging, sagas, or deadlines. |
What gets converted
The recipes handle the following categories of changes automatically:
-
Build files:BOM rename, Java source/target version bump, Spring Boot upgrade to 3.5, removal of dependencies with no Axon Framework 5 equivalent
-
Package and class renames:handler annotations,
EventBus→EventSink,ConfigurerModule→ConfigurationEnhancer, extension packages -
Annotation rewrites:`@Aggregate` →
@EventSourced,@AggregateRoot→@EventSourcedEntity,@TargetAggregateIdentifier→@TargetEntityId,@Revision→@Event(version = …),@ProcessingGroup→@Namespace,@EntityCreatoradded to constructorsAF4’s
@TargetAggregateIdentifierwas both the target-id and the command’s routing key, keeping same-entity commands handled sequentially. In AF5 that routing key must be explicit as@Command(routingKey = "…"). Whether the OpenRewrite rewrite lifts it for you depends on the recipe version: older versions rename@TargetAggregateIdentifierto@TargetEntityIdonly and leave a bare (or absent)@Command, while newer versions also fill in theroutingKey. Do not assume either way; verify each command and reconcile it to the correct end state regardless of what the rewrite produced. See Target entity identification for the required end state. -
Message API:accessor method renames,
AggregateLifecycle.apply(…)replaced withEventAppender#append(…) -
Command handling:`@CommandHandler` constructors converted to static
handle(…)methods,CommandGatewayreplaced withCommandDispatcher -
Legacy sagas (commercial recipe only):`axoniq-legacy` added when saga types are detected, static
SagaLifecyclecalls moved to an injected handler parameter, a closingSagaLifecycle.end()in a@DeadlineHandlerwithout an earlyreturnreplaced with@EndSaga, the other staticSagaLifecyclecalls of a@DeadlineHandler, such as a conditional end or one an earlyreturncan skip, called throughSagaLifecycle.forContext(processingContext)on aProcessingContexthandler parameter, and injectedCommandGatewayfields, also those only@DeadlineHandlermethods use, replaced withCommandDispatcherhandler parameters. Other injected saga dependencies must be moved to handler parameters manually; the recipe marks those fields, and anyResourceInjectorusage, with aTODO(axon4to5)comment. StaticSagaLifecyclecalls in other saga methods, such as private helpers, are marked the same way, because the legacy module injectsSagaLifecycleinto@SagaEventHandlermethods only and each caller would have to pass itsProcessingContexton. See Saga upgrade.CommandGateway.send(…)remains fire-and-forget, whileCommandGateway.sendAndWait(…)remains synchronous. -
Legacy deadlines (commercial recipe only,
Axon4ToAxoniq5LegacyDeadline):`axoniq-legacy` added when deadline types are detected, withDeadlineManagerand@DeadlineHandlerkept in their Axon Framework 4 packages. An injected privateDeadlineManagerfield of a saga is moved to a parameter of the handlers that use it; any other is marked like the other injected saga fields. A@DeadlineHandlerwithout parameters first gets aDeadlineMessageparameter, because the first parameter of a handler is its payload unless a higher-priority resolver claims it, and aDeadlineManagerresolved from the configuration does not. A deadline scheduled for an aggregate fires as a command, so the aggregate’s@DeadlineHandlerbecomes the@CommandHandlerthat receives it. Because only saga handlers run inside a scope, aggregateDeadlineManagercalls that relied on the current scope receive an explicitAggregateScopeDescriptor. Both aggregate rewrites apply to Java sources only; in Kotlin aggregates the deadline handlers and those calls are marked with aTODO(axon4to5)comment instead. The deadline manager builders losespanFactory(..)andtransactionManager(..)and get the requiredunitOfWorkFactory(..), looked up on an Axon Framework 4Configurationparameter or local in scope or, in a Spring@Beanmethod, taken from aUnitOfWorkFactoryparameter the recipe adds; a default Jacksonserializer(..)becomesconverter(..), and so does a serializer a Spring@Beanmethod receives as a parameter, which becomes the matchingEventConverter,MessageConverterorGeneralConverterparameter, with aTODO(axon4to5)comment to check that converter against the deadlines already stored by the Axon Framework 4 serializer; andConfigurationScopeAwareProviderandConfiguration#deadlineManager()are replaced with component lookups. The recipe leaves aTODO(axon4to5)comment where the replacement is a decision of the application: aunitOfWorkFactory(..)with neither in reach, a converter it cannot infer,configureDeadlineManager(..), interceptors registered on theDeadlineManagerinterface, aScopeAwareProviderregistered by the application, aggregate deadline handlers no command can reach (including a handler matching by deadline name alone, unless every schedule call of that deadline passes no payload), deadline handlers of member entities, and aggregateDeadlineManagercalls in member entities, in static methods or constructors that are no creation command handler, and in creation command handlers or@CreationPolicyhandlers that may create the aggregate whose command has no single@TargetAggregateIdentifierreadable through a record accessor, getter or public field. In creation handlers with such a command, those calls receive the command’s target identifier. -
Query API:`ResponseTypes.instanceOf(X.class)` unwrapped to a direct class argument,
query()renamed toqueryMany()when used withmultipleInstancesOf -
Spring configuration:`axon.serializer.` properties renamed to
axon.converter. -
Test fixtures:`AggregateTestFixture` replaced with
AxonTestFixture, Given-When-Then chain rewritten.AxonTestFixturehas no deadline manager and no clock, so the aggregate fixture’s deadline assertions, time calls such asgivenCurrentTimeandwhenTimeElapses, and deadline interceptor registrations are kept and marked with aTODO(axon4to5)comment -
Legacy test fixtures (commercial recipe only):`SagaTestFixture` and
StubDeadlineManagerkept throughaxoniq-legacy-test, interceptor lambdas registered on them rewritten to the Axon Framework 5 signature, and fixture calls the portedSagaTestFixtureno longer supports, such as the scheduled event assertions, marked with aTODO(axon4to5)comment. On aggregate tests, that comment explains how to test a deadline as the command it now fires as
Running the recipe
Maven
Run the commercial recipe against your project:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:6.46.1:run \
-Drewrite.recipeArtifactCoordinates=org.axonframework:axon-migration:<version> \
-Drewrite.activeRecipes=io.axoniq.framework.migration.UpgradeAxon4ToAxoniq5
|
Keep the plugin at 6.46.1 or newer. An older plugin fails inside the recipe with a |
For the free (Apache 2.0) path, replace the active recipe with org.axonframework.migration.UpgradeAxon4ToAxon5.
Replace <version> with the Axon Framework 5 release you are targeting (for example, 5.4.0).
Gradle
Gradle projects do not carry the OpenRewrite plugin by default.
The init.gradle script bundled with the migration module wires it in without modifying your build files.
Copy migration/init.gradle into your target project (or reference it by absolute path), then run:
gradle rewriteRun --init-script init.gradle \
-Drewrite.activeRecipe=io.axoniq.framework.migration.UpgradeAxon4ToAxoniq5
For the free (Apache 2.0) path, replace the active recipe with org.axonframework.migration.UpgradeAxon4ToAxon5.
|
Do not combine the init script with a |
To override the recipe artifact version, add -Drewrite.axonMigrationVersion=<version> to the command.
For further background, see the OpenRewrite guide on running recipes on a Gradle project without modifying the build.
After running the recipe
Once the recipes complete, the project state will look like this:
-
Build files are updated—dependency versions, BOM coordinates, and Spring Boot version are aligned with Axon Framework 5.
-
Many Java (and Kotlin) source files have been rewritten—renames, annotation rewrites, and API changes applied mechanically.
-
The project does not compile. The remaining compile errors identify exactly which API changes require manual attention.
-
TODO(axon4to5)markers in the source point to the locations where the recipe left work intentionally unfinished.
Work through the compile errors and markers by consulting the upgrade paths.
|
For version 5.4.0, use |