Saga Migration
Axon Framework 5 has no Saga construct. Replace it with existing core framework building blocks.
The familiar @Saga stereotype and related APIs are not part of the Axon Framework 5 core; a legacy layer allows
migrating running sagas, and the same applies to DeadlineManager and related APIs.
For new processes, Axon Framework 5 provides several ways to implement them with the existing framework building blocks. The Sagas and Process Managers guide describes them in detail.
For sagas that are already running (considering business processes running for months), rewriting is not an option.
That case is served by the axon-legacy module, described in Running existing sagas on Axon Framework 5.
|
For saga migration you need to consider two scenarios: Writing new processes: Pick an approach from the Sagas and Process Managers guide and write it today. Migrating running sagas: Use the Axon Framework 4 compatible APIs of the |
Running existing sagas on Axon Framework 5
The axon-legacy module allows migrating to Axon Framework 5 while saga instances started on Axon Framework 4 are still running.
Business processes covered by sagas can span weeks or months.
axon-legacy lets running saga instances continue while new instances start on an Axon Framework 5 native implementation of the business process.
The axon-legacy module brings forward the following Axon Framework 4 saga infrastructure, adapted to the Axon Framework 5 APIs:
-
@SagaEventHandler,@StartSaga,@EndSaga,SagaLifecycleand the association-value mechanism -
SagaStoreand its concrete implementations, to support existing saga stores
The org.axonframework.spring.stereotype.Saga annotation itself keeps its Axon Framework 4 package. Spring Boot autoconfiguration for legacy sagas activates only
when both axon-legacy and axon-spring-boot-starter are on the classpath; see Configuring legacy sagas with Spring Boot.
The legacy module supports existing saga classes as-is. Add axon-legacy as a dependency to your project.
|
The saga side of |
The axon-legacy module is a migration aid, not a supported way to write new processes. @StartSaga is deprecated.
Use the legacy module to let existing instances receive their events and run to completion while new work goes to the
Axon Framework 5 implementation written alongside it. A ported @StartSaga handler can still create instances until
you remove or disable that annotation as part of the drainage strategy.
Configuring legacy sagas with Spring Boot
Add axon-legacy next to axon-spring-boot-starter as a dependency. Autoconfiguration for legacy sagas activates
once axon-legacy is on the classpath; no further setup is needed for a @Saga-annotated, component-scanned class to
start receiving events.
Processor assignment
Each @Saga class gets its own pooled streaming event processor, named <SimpleName>Processor (for example,
OrderSagaProcessor for OrderSaga). The processor starts at the head of the stream (the latest token), so a saga
ignores events that occurred before the processor first starts, matching the Axon Framework 4
TrackingEventProcessor default for sagas.
Tuning a saga’s processor does not change which events it sees. The head token survives every
axon.eventhandling.processors.<name>.* entry and any EventProcessorDefinition that does not set an initial token
of its own.
To make a saga process the stream from the start, set the initial token explicitly with a SagaProcessorDefinition:
@Bean
SagaProcessorDefinition replayIntoOrderSaga() {
return SagaProcessorDefinition.forSaga(OrderSaga.class)
.whenPooledStreaming(config -> config.initialToken(
source -> source.firstToken(null)
));
}
Every axon.eventhandling.processors.<name>.* property (mode, source, token-claim-interval, thread-count,
batch-size, and token-store) applies to a saga’s processor as it applies to any other processor. A pooled saga
processor needs a TokenStore bean, as does any other pooled processor.
initial-segment-count is the exception. A saga’s processor starts with a single segment, and the property does not
raise it; raise the count with a SagaProcessorDefinition, see Configuring a saga’s processor. Segments do not
split the stream: every segment reads every event and keeps only the sagas it owns. A higher count lets several
threads process sagas concurrently within an application instance.
|
|
Processor names derived from a saga’s simple name are mixed-case (for example, SimpleSagaProcessor). Relaxed
property binding lowercases a dotted key and silently fails to match, so target the processor with bracket notation
instead:
axon.eventhandling.processors[SimpleSagaProcessor].mode=subscribing
@Namespace on the saga type replaces @ProcessingGroup: it overrides the derived processor name. Two saga classes
carrying the same @Namespace value (or deriving the same default name) share one processor, reproducing the Axon
Framework 4 behavior of co-locating sagas in one processing group.
|
Keep the original processor name, or in-flight sagas lose events.
Replace
This applies to any saga whose processor was not already named |
Processor settings and EventProcessorDefinition beans apply to a saga’s processor as they apply to any other, and
an EventProcessorDefinition selector can assign a saga to its named processor.
Configuring a saga’s processor
Declare a SagaProcessorDefinition bean. It runs after the saga defaults and the processor properties, so it has
the last word on the processor’s configuration:
@Bean
SagaProcessorDefinition orderSagaSegments() {
return SagaProcessorDefinition.forSaga(OrderSaga.class)
.whenPooledStreaming(config -> config.initialSegmentCount(4));
}
Choose the processor in one of two ways:
forSaga(OrderSaga.class)-
the processor carrying that saga, wherever it ended up, including a
@Namespace-renamed one. The saga type picks the processor, it does not narrow the customization to that saga, so when sagas share a processor this configures all of them. A log message names the others. forProcessor("orders")-
the processor with that name. Use it for a processor several sagas share.
// Both OrderSaga and ShipmentSaga carry @Namespace("orders"), so they share one processor.
@Bean
SagaProcessorDefinition ordersProcessor() {
return SagaProcessorDefinition.forProcessor("orders")
.whenPooledStreaming(config -> config.batchSize(50));
}
Then choose how much of the configuration to reach:
customized(…)-
the settings both processor modes share: the error handler, the unit of work factory, and extensions. Applies whatever mode the processor runs in.
whenPooledStreaming(…)-
the
PooledStreamingEventProcessorConfiguration, with the initial token and segment count. Applies only while the processor runs in pooled streaming mode. whenSubscribing(…)-
the
SubscribingEventProcessorConfiguration, with itseventSourceandignoredMessageHandler. Applies only while the processor runs in subscribing mode.
// Applies only when axon.eventhandling.processors[OrderSagaProcessor].mode=subscribing.
@Bean
SagaProcessorDefinition orderSagaEventSource(SubscribableEventSource eventSource) {
return SagaProcessorDefinition.forSaga(OrderSaga.class)
.whenSubscribing(config -> config.eventSource(eventSource));
}
Naming a mode does not switch the processor to it. The mode comes from axon.eventhandling.processors.<name>.mode or
a matching EventProcessorDefinition; a definition written for the other mode is skipped and logged.
|
An |
A saga and an ordinary event handler cannot share a processor. When both resolve to the same name, startup fails with
a DuplicateModuleRegistrationException. To put them on one processor on purpose, drop @Saga from the class and
register a Module bean combining a declarative saga component with autodetected handlers.
Saga store selection
A saga’s store is resolved in this order:
-
A user-defined
SagaStorebean. -
A
JpaSagaStore(bean namesagaStore), when anEntityManagerFactorybean is present.SagaEntryandAssociationValueEntryare registered with the persistence unit automatically. -
A
JdbcSagaStore(bean namesagaStoreNoSchemaorsagaStoreWithSchema), when aDataSourcebean is present. A user-declaredSagaSqlSchemabean selects the schema-aware variant. -
An
InMemorySagaStore(bean namesagaStore), as the final fallback.
@Saga(sagaStore = "beanName") overrides the store for one saga type, resolving the named bean instead of the one in
the chain above.
Suppressing autoconfiguration for one saga
Discovery registers a SpringSagaDescriptor bean named <sagaBeanName>$$Registrar for every @Saga bean. Declaring
a bean under that name yourself keeps discovery from configuring that saga.
|
If you want to opt out a specific saga from If a |
Behavior changes from Axon Framework 4
-
Pooled streaming replaces the tracking processor. A saga’s processor is pooled streaming, not a
TrackingEventProcessor, and needs aTokenStorebean as any pooled processor does. A saga still only sees events published after its processor first started; replaying into one requires setting an initial token explicitly, see Configuring legacy sagas with Spring Boot. -
No resource injection.
ResourceInjectorandSpringResourceInjectorare not ported. A saga’s collaborators arrive as handler method parameters instead, resolved the same way as for any other event handler, including Spring beans. -
No
@ProcessingGroup.@Namespaceon the saga type renames or co-locates a saga’s processor. It co-locates sagas with each other only: a saga and an ordinary event handler resolving to one processor name fail at startup. See Configuring legacy sagas with Spring Boot for the workaround. -
Dead-letter queues are not supported for sagas. Enabling a dead letter queue for a saga’s processor by name has no effect.
-
Deadlines are not ported yet (see the note in Running existing sagas on Axon Framework 5).
-
The in-memory saga store is an explicit, overridable bean, not an internal default. An application that wants a default store alongside named per-saga stores declares the default store itself.
-
A
@Sagaclass must be public with an accessible no-argument constructor. -
Component-scan saga classes, or declare them with a
@Scope("prototype")annotated@Beanmethod. A plain@Beanmethod ignores the@Scope("prototype")that@Sagadeclares and requires to avoid the saga being registered additionally as a plain singleton event handling component bean. Annotate the bean method with@Scope("prototype")if the saga cannot be component-scanned.
Mapping Axon Framework 4 saga concepts to Axon Framework 5
An Axon Framework 4 saga provides opinionated technical infrastructure to implement long-running business processes:
-
State is state-sourced: the saga instance is serialized into a
SagaStoreas an opaque blob -
Correlation requires association values, maintained with
SagaLifecycle.associateWith(…). -
Life cycle is framework-managed, through
@StartSagaand@EndSaga.
Axon Framework 5 provides several ways to implement long-running processes, described in Sagas and Process Managers. The following table maps Axon Framework 4 saga concepts to their Axon Framework 5 equivalents.
| Axon Framework 4 | Axon Framework 5 |
|---|---|
|
A |
|
|
|
|
|
The end of the workflow definition for a Workflow. For a plain |
|
|
|
a |
|
Your regular event store for a Workflow, a repository of your own, an event-sourced entity, or nothing. |
|
|
|
The dedicated Workflows Test infrastructure, or |
Replacing a saga with a workflow
The recommended replacement for sagas is Workflows: a process is written as a single method that reads top to bottom, using the regular event store for event-sourced persistence.
|
Workflows are an Axoniq Framework feature and require an Axoniq license to run in production. Workflows are currently available as a preview and can be tried out without a license. |
Saga concepts map onto Workflow concepts:
-
The association value becomes the workflow’s identifier and manual
associate(…)invocations when waiting on events. -
Each
@SagaEventHandlerbecomes a step. -
@EndSagabecomes the end of the workflow definition method. -
Deadlines are expressed as explicit wait or timeout on an event in the workflow.
Workflows differ from Axon Framework 4 sagas in how they hold state: they source it from dedicated workflow events in
the regular event store instead of a persistent SagaStore blob.
A running saga holding a serialized instance in a SagaStore cannot be handed to a workflow. It must drain as
described in Migrating an application with running sagas, whichever replacement you pick.
Migrating deadlines
Axon Framework 4 scheduled deadlines to defer reactions for sagas. Axon Framework 5 has no built-in deadline scheduling. Use other capabilities to get the same effect.
In Axon Framework 4, DeadlineManager schedules a deadline to trigger in the future; @DeadlineHandler handles the
trigger, typically by sending a command.
In Axon Framework 5, use a scheduling library of your choice: wire in the CommandGateway, and send the command
through the gateway when the scheduled task triggers.
An idempotent command handler ignores an action scheduled for a process that has already progressed.
See deadlines for details on deadlines for long-running processes in Axon Framework 5.
Choosing a replacement
Not every saga should become a workflow. Simple processes consisting of a limited set of independent reactions can use regular Axon Framework 5 event and command handlers. The guide compares the alternatives in detail:
-
State in a repository is the closest to how an Axon Framework 4 saga worked. Start here if you are porting an existing saga.
-
State from context events avoids persistent state in a repository. This is only possible if all involved contexts write to one event store and their events carry a tag you can select on.
-
State from process events suits integration with systems whose events are not yours, and gives an audit trail of the process.
-
Vertical slices break the process into independent reactions. This fits Vertical Slice Architecture and Event Modelling.
Migrating an application with running sagas
Whether or not you use the legacy module, migrate running saga instances by drainage: let saga instances created by Axon Framework 4 finish normally, while new process instances start on an Axon Framework 5 implementation.
Without the legacy module, drain old saga instances in an Axon Framework 4 deployment kept alive alongside the Axon Framework 5 application running the migrated process. The legacy module allows you to drain the old saga instances alongside the upgraded application in a single Axon Framework 5 application. Either way (using the legacy-module for drainage or not), new sagas must not be started from the Axon Framework 4 legacy code.
The main steps for migrating running sagas:
-
Stop starting new sagas. Remove the
@StartSagaannotation from the Axon Framework 4 saga. Existing instances keep receiving their events and run to completion; no new ones are created.@Saga public class PaymentSaga { // @StartSaga (1) @SagaEventHandler(associationProperty = "bikeId") public void on(BikeRequestedEvent event) { // ... } }1 Removing this annotation stops new instances from starting; existing instances can still advance and end. -
Keep the old saga running. With the legacy module, add the dependency and let the existing class continue against its existing saga store in the upgraded application. Without it, keep the Axon Framework 4 deployment alive to drain existing saga instances without creating new ones.
-
Implement the process in Axon Framework 5 using one of the approaches above, and let it handle the same trigger event.
-
Wait for the old instances to drain. Monitor the
SagaStorefor active instances and watch the count fall to zero. How long that takes depends on your longest-running process. -
Retire the Axon Framework 4 saga once there are no more active instances in the
SagaStore: remove the legacy saga code, theSagaStoretable, its deadline scheduler, and the legacy dependency.
Scheduled deadlines drain the same way: keep the Axon Framework 4 DeadlineManager, or the legacy module’s port of
it, online as long as saga instances still rely on it for deadlines.
Testing
A saga kept running on the legacy module keeps its existing tests.
Write new tests for the process migrated to Axon Framework 5 with AxonTestFixture: publish events that drive the
process and assert on the commands it sends.
The mapping below covers the common cases:
SagaTestFixture |
AxonTestFixture |
|---|---|
|
|
|
|
|
|
|
|
|
No equivalent. Assert on the observable outcome, or on your own state if the approach keeps any. |
|
No equivalent. Structure the timeout so it can be invoked directly; see deadlines. |
A working example
examples/saga-recipes in the
Axon Framework repository implements the bike-rental payment saga from an Axon Framework 4 sample application, using every approach described here.