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 TODO(axon4to5) markers left in the source, form a guided checklist of everything that still needs your attention.

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

org.axonframework.migration.UpgradeAxon4ToAxon5

Upgrades an Axon Framework 4 application to Axon Framework 5. Renames packages and classes within the org.axonframework.* namespace and applies all core source rewrites.

You are targeting free Axon Framework 5 and your AF4 application does not use Axon Server connector, the sequenced dead-letter queue, DistributedCommandBus, sagas, or deadlines. Warning: if your application does use those features, this recipe alone leaves the codebase non-compiling—use UpgradeAxon4ToAxoniq5 instead.

io.axoniq.framework.migration.UpgradeAxon4ToAxoniq5

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, @EntityCreator added to constructors

    AF4’s @TargetAggregateIdentifier was 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 @TargetAggregateIdentifier to @TargetEntityId only and leave a bare (or absent) @Command, while newer versions also fill in the routingKey. 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 with EventAppender#append(…​)

  • Command handling:`@CommandHandler` constructors converted to static handle(…​) methods, CommandGateway replaced with CommandDispatcher

  • Legacy sagas (commercial recipe only):`axoniq-legacy` added when saga types are detected, static SagaLifecycle calls moved to an injected handler parameter, a closing SagaLifecycle.end() in a @DeadlineHandler without an early return replaced with @EndSaga, the other static SagaLifecycle calls of a @DeadlineHandler, such as a conditional end or one an early return can skip, called through SagaLifecycle.forContext(processingContext) on a ProcessingContext handler parameter, and injected CommandGateway fields, also those only @DeadlineHandler methods use, replaced with CommandDispatcher handler parameters. Other injected saga dependencies must be moved to handler parameters manually; the recipe marks those fields, and any ResourceInjector usage, with a TODO(axon4to5) comment. Static SagaLifecycle calls in other saga methods, such as private helpers, are marked the same way, because the legacy module injects SagaLifecycle into @SagaEventHandler methods only and each caller would have to pass its ProcessingContext on. See Saga upgrade. CommandGateway.send(…​) remains fire-and-forget, while CommandGateway.sendAndWait(…​) remains synchronous.

  • Legacy deadlines (commercial recipe only, Axon4ToAxoniq5LegacyDeadline):`axoniq-legacy` added when deadline types are detected, with DeadlineManager and @DeadlineHandler kept in their Axon Framework 4 packages. An injected private DeadlineManager field 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 @DeadlineHandler without parameters first gets a DeadlineMessage parameter, because the first parameter of a handler is its payload unless a higher-priority resolver claims it, and a DeadlineManager resolved from the configuration does not. A deadline scheduled for an aggregate fires as a command, so the aggregate’s @DeadlineHandler becomes the @CommandHandler that receives it. Because only saga handlers run inside a scope, aggregate DeadlineManager calls that relied on the current scope receive an explicit AggregateScopeDescriptor. Both aggregate rewrites apply to Java sources only; in Kotlin aggregates the deadline handlers and those calls are marked with a TODO(axon4to5) comment instead. The deadline manager builders lose spanFactory(..) and transactionManager(..) and get the required unitOfWorkFactory(..), looked up on an Axon Framework 4 Configuration parameter or local in scope or, in a Spring @Bean method, taken from a UnitOfWorkFactory parameter the recipe adds; a default Jackson serializer(..) becomes converter(..), and so does a serializer a Spring @Bean method receives as a parameter, which becomes the matching EventConverter, MessageConverter or GeneralConverter parameter, with a TODO(axon4to5) comment to check that converter against the deadlines already stored by the Axon Framework 4 serializer; and ConfigurationScopeAwareProvider and Configuration#deadlineManager() are replaced with component lookups. The recipe leaves a TODO(axon4to5) comment where the replacement is a decision of the application: a unitOfWorkFactory(..) with neither in reach, a converter it cannot infer, configureDeadlineManager(..), interceptors registered on the DeadlineManager interface, a ScopeAwareProvider registered 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 aggregate DeadlineManager calls in member entities, in static methods or constructors that are no creation command handler, and in creation command handlers or @CreationPolicy handlers that may create the aggregate whose command has no single @TargetAggregateIdentifier readable 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 to queryMany() when used with multipleInstancesOf

  • Spring configuration:`axon.serializer.` properties renamed to axon.converter.

  • Test fixtures:`AggregateTestFixture` replaced with AxonTestFixture, Given-When-Then chain rewritten. AxonTestFixture has no deadline manager and no clock, so the aggregate fixture’s deadline assertions, time calls such as givenCurrentTime and whenTimeElapses, and deadline interceptor registrations are kept and marked with a TODO(axon4to5) comment

  • Legacy test fixtures (commercial recipe only):`SagaTestFixture` and StubDeadlineManager kept through axoniq-legacy-test, interceptor lambdas registered on them rewritten to the Axon Framework 5 signature, and fixture calls the ported SagaTestFixture no longer supports, such as the scheduled event assertions, marked with a TODO(axon4to5) comment. On aggregate tests, that comment explains how to test a deadline as the command it now fires as

Upgrade markers

Where a change cannot be automated safely, the recipe leaves a marker comment so the location is easy to find:

  • Java / Kotlin: // TODO(axon4to5): <action>

  • Properties / YAML: # TODO(axon4to5): <action>

After running, collect all such markers with:

grep -r "TODO(axon4to5)" src/

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 NoSuchMethodError and then deletes every source file of the project. Run on a clean git working tree.

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 rewrite { } block in build.gradle or build.gradle.kts—the OpenRewrite Gradle plugin only supports one configuration source at a time.

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:

  1. Build files are updated—dependency versions, BOM coordinates, and Spring Boot version are aligned with Axon Framework 5.

  2. Many Java (and Kotlin) source files have been rewritten—renames, annotation rewrites, and API changes applied mechanically.

  3. The project does not compile. The remaining compile errors identify exactly which API changes require manual attention.

  4. 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 io.axoniq.framework:axoniq-legacy and io.axoniq.framework:axoniq-legacy-test. If recipe output contains the old org.axonframework:axon-legacy or axon-legacy-test coordinates, replace them. Applications using the Axoniq Spring Boot integration require io.axoniq.framework:axoniq-spring-boot-starter. Verify the resolved dependency graph after conversion.

Scope and limitations

The following are explicitly out of scope for code conversion:

  • No data changes:event store contents, token tables, dead-letter queue entries, and saga state are never touched.

  • No compile guarantee:a non-compiling project after the run is the expected outcome, not an error.