Sequencing Policy Migration
Axon Framework 5 refines the SequencingPolicy concept, moving it from event-only processing to a shared abstraction for both events and commands.
This path covers the migration of sequencing policies from Axon Framework 4 to Axon Framework 5.
For a complete understanding of how sequencing policies work in Axon Framework 5, see Sequencing Policies.
|
The most notable changes are:
|
API changes
The SequencingPolicy interface has been redesigned to be reusable for message sequencing in general (not only event sequencing) and fit the context-aware architecture of Axon Framework 5.
-
Axon Framework 4
-
Axon Framework 5
package org.axonframework.eventhandling.async;
public interface SequencingPolicy<T> {
Object getSequenceIdentifierFor(T event);
}
Key characteristics:
-
The type parameter
Trefers to the event type directly -
Returns
nullto indicate no sequencing requirement (full concurrency for that message) -
No access to processing context
package org.axonframework.messaging.core.sequencing;
public interface SequencingPolicy<M extends Message> {
Optional<Object> sequenceIdentifierFor(M message, ProcessingContext context);
}
Key changes:
-
The type parameter
Mis bound toMessage, making the policy applicable to any message type -
Returns
Optional.empty()instead ofnullto indicate no need for sequencing -
Receives an explicit
ProcessingContextfor resource access
When migrating a custom SequencingPolicy, apply the following changes:
-
Axon Framework 4
-
Axon Framework 5
public class CustomerIdSequencingPolicy implements SequencingPolicy<EventMessage<?>> {
@Override
public Object getSequenceIdentifierFor(EventMessage<?> event) {
if (event.getPayload() instanceof CustomerEvent customerEvent) {
return customerEvent.getCustomerId();
}
return null;
}
}
public class CustomerIdSequencingPolicy implements SequencingPolicy<EventMessage> {
@Override
public Optional<Object> sequenceIdentifierFor(EventMessage message, ProcessingContext context) {
if (message.payload() instanceof CustomerEvent customerEvent) {
return Optional.of(customerEvent.customerId());
}
return Optional.empty();
}
}
Changed default: from SequentialPerAggregatePolicy to HierarchicalSequencingPolicy
In Axon Framework 4, the default sequencing policy for streaming event processors was SequentialPerAggregatePolicy.
This policy groups events by aggregate identifier, so events from the same aggregate are processed sequentially, while events from different aggregates can be processed concurrently.
For non-domain events (events without an aggregate identifier), the policy returned null, resulting in full concurrency for those events.
In Axon Framework 5, the default is a HierarchicalSequencingPolicy that wraps SequentialPerAggregatePolicy as its primary policy, with SequentialPolicy as the fallback.
This means that when the primary policy returns Optional.empty()—which happens when no aggregate identifier is found in the processing context—the fallback SequentialPolicy is applied instead.
The practical impact depends on your event store solution:
-
Aggregate-based event store solution: Events carry an aggregate identifier that is placed in the processing context.
SequentialPerAggregatePolicycan find it, so the behavior is identical to Axon Framework 4—events from the same aggregate are handled sequentially, events from different aggregates can be processed concurrently. Users migrating from Axon Framework 4 without migrating their event store to DCB should observe no behavioral difference. -
DCB (tag-based) event store solution: Events are stored using tags rather than aggregate identifiers. No aggregate identifier is placed in the processing context, so
SequentialPerAggregatePolicyreturnsOptional.empty()for every event. TheHierarchicalSequencingPolicythen falls back toSequentialPolicy, processing all events sequentially within a single segment (allowing no concurrency at all within the processing segment). This is the safe default, but limits parallelism—see the section below on defining an explicit policy.
Define an explicit event sequencing policy
Because the default policy adapts automatically to your event store type, it can introduce unexpected performance implications when migrating to a DCB-based event store. We therefore strongly recommend always defining a sequencing policy that explicitly matches your application’s ordering requirements.
For the available built-in policies and configuration options, see Sequencing Policies. The two main configuration approaches are shown below.
Annotation-based configuration
Use @SequencingPolicy on an event handling component class or on individual @EventHandler methods.
Method-level annotations take precedence over class-level ones.
import org.axonframework.messaging.eventhandling.annotation.EventHandler;
import org.axonframework.messaging.core.annotation.SequencingPolicy;
import org.axonframework.messaging.core.sequencing.PropertySequencingPolicy;
@SequencingPolicy(type = PropertySequencingPolicy.class, parameters = {"customerId"})
class OrderEventHandler {
@EventHandler
public void on(OrderPlacedEvent event) {
// handled sequentially per customerId
}
}
Declarative configuration
Register the sequencing policy programmatically through the MessagingConfigurer.
import org.axonframework.messaging.core.configuration.MessagingConfigurer;
import org.axonframework.messaging.core.sequencing.PropertySequencingPolicy;
import org.axonframework.messaging.eventhandling.SimpleEventHandlingComponent;
public void configureEventProcessing(MessagingConfigurer configurer) {
configurer.eventProcessing(eventConfigurer ->
eventConfigurer.pooledStreaming(pooledStreamingConfigurer ->
pooledStreamingConfigurer.processor("order-processor", customizer ->
customizer.eventHandlingComponents(ehc ->
ehc.declarative(cfg ->
SimpleEventHandlingComponent.create("orders",
new PropertySequencingPolicy<>(Order.class, "customerId")
)
)
).notCustomized()
)
)
);
}
Sequencing policies for commands
In Axon Framework 4, sequencing policies exclusively applied to event processing.
In Axon Framework 5, the SequencingPolicy abstraction is also used for command handling.
By default, the RoutingKeySequencingPolicy sequences commands by their routing key, ensuring that commands targeting the same entity are handled sequentially and eliminating optimistic locking failures in high-throughput scenarios.
This change does not introduce migration issues for existing applications, as command sequencing is new functionality that operates transparently in the background and reflects the aggregate locking behaviour that was the default in Axon Framework 4. If you need to customize or disable command sequencing, see Command sequencing.