Infrastructure

Command handling in Axon Framework involves several infrastructure components that work together to dispatch commands, manage entity state, and coordinate command processing across your application.

This page covers the key infrastructure components you’ll work with:

  • Repositories: Manage entity lifecycle and state persistence

  • Command bus: Routes and dispatches commands to handlers

  • Command gateway: Provides a convenient API for dispatching commands

  • Command handling components: Register and manage command handlers

Repositories

Repositories provide an abstraction for loading and persisting entities. When you use entities for stateful command handling (see Command handlers), repositories manage the entity lifecycle automatically.

Why repositories?

Repositories handle several concerns for you:

  • Loading entity state: Fetch current state from storage (event store or database)

  • Lifecycle management: Track entity instances during command processing

  • Persistence: Save state changes when command processing completes

  • Coordination: Ensure changes are committed as part of the processing context

Without a repository, you would need to manually load entities, track changes, and persist them. Repositories do all of this automatically.

Async-native API

Repository operations in Axon Framework are async-native, returning CompletableFuture to support non-blocking operations:

public interface Repository<ID, E> {
    // Load an existing entity
    CompletableFuture<ManagedEntity<ID, E>> load(ID identifier,
                                                 ProcessingContext context);

    // Load existing or create new entity
    CompletableFuture<ManagedEntity<ID, E>> loadOrCreate(ID identifier,
                                                         ProcessingContext context);

    // Persist a new entity
    ManagedEntity<ID, E> persist(ID identifier, E entity,
                                 ProcessingContext context);
}

All repository operations require an active ProcessingContext. Command handlers automatically receive a ProcessingContext parameter that you pass to repository methods.

ManagedEntity

Repositories return entities wrapped in a ManagedEntity interface. This wrapper tracks the entity’s lifecycle and enables the repository to detect and persist changes.

public interface ManagedEntity<ID, E> {
    ID identifier();           // Get the entity identifier
    E entity();               // Get the current entity state
    E applyStateChange(UnaryOperator<E> change);  // Modify entity state
}

The ManagedEntity wrapper ensures that:

  • Changes to entity state are tracked

  • State modifications are persisted when the processing context commits

  • The repository maintains consistent state throughout command processing

Using repositories in command handlers

When command handlers are not on the entity itself, you use a repository to load and manage entities manually. This pattern is covered in Command-centric stateful handlers.

@Component
public class OrderCommandHandler {

    private final Repository<String, Order> orderRepository;

    public OrderCommandHandler(Repository<String, Order> orderRepository) {
        this.orderRepository = orderRepository;
    }

    @CommandHandler
    public void handle(ShipOrderCommand command,
                      ProcessingContext context,
                      EventAppender eventAppender) {
        // Load the entity
        orderRepository.load(command.orderId(), context)
            .thenAccept(managedOrder -> {
                Order order = managedOrder.entity();
                // Apply business logic
                if (order.canShip()) {
                    eventAppender.append(new OrderShippedEvent(command.orderId()));
                }
            });
    }
}

EventSourcedRepository

The most common repository implementation is EventSourcedRepository, which manages event-sourced entities.

Configuration with Spring Boot:

import org.axonframework.extension.spring.stereotype.EventSourced;

@EventSourced
public class Order {

    // Omitted handlers and state for brevity.
}

Configuration with Configuration API:

import org.axonframework.eventsourcing.annotation.EventSourcedEntity;
import org.axonframework.eventsourcing.configuration.EventSourcingConfigurer;

@EventSourcedEntity
public class Order {

    // Omitted handlers and state for brevity.
}

public class AxonConfig {

    public void configureOrderEntity(EventSourcingConfigurer configurer) {
        configurer.registerEntity(
                EventSourcedEntityModule.autodetected(String.class, Order.class)
        );
    }
}

Command bus

The command bus is responsible for routing commands to their registered handlers. It receives commands from gateways or dispatchers and invokes the appropriate command handler.

SimpleCommandBus

The SimpleCommandBus is the standard command bus implementation.

Key characteristics:

  • Simple and reliable: Straightforward processing model

  • Good performance: Low overhead for command handling

  • Single JVM: Cannot distribute commands across multiple instances

Spring Boot automatically configures a SimpleCommandBus when using the Axon Spring Boot starter (unless Axon Server is configured).

Manual configuration:

  • Configuration API

  • Spring Boot

import org.axonframework.messaging.core.configuration.MessagingConfigurer;

public class AxonConfig {

    public void configureCommandBus(MessagingConfigurer configurer) {
        configurer.registerCommandBus(
                config -> new SimpleCommandBus(
                        config.getComponent(UnitOfWorkFactory.class)
                )
        );
    }
}
@Configuration
public class AxonConfig {

    @Bean
    public CommandBus commandBus(UnitOfWorkFactory unitOfWorkFactory) {
        return new SimpleCommandBus(unitOfWorkFactory);
    }
}

Interceptors

The command bus supports interceptors for cross-cutting concerns like logging, metrics, or validation.

There are two types of interceptors:

  • Dispatch interceptors: Invoked when a command is dispatched, before routing

  • Handler interceptors: Invoked before the actual command handler, after routing

See Command interceptors for details on implementing and registering interceptors.

Automatic command sequencing: The command bus automatically includes a sequencing interceptor that prevents concurrent commands from causing optimistic locking failures. Commands targeting the same entities (same routing key) are processed sequentially while maintaining full concurrency for different entities. This is transparent and requires no configuration in most cases.

For customization options, see Command sequencing.

Retry behavior

When a command dispatch fails, the framework can automatically retry it if a RetryScheduler is registered in the configuration. The RetryScheduler decides, based on the failure and the dispatch history, whether another attempt is warranted and how long to wait before trying again. No additional wiring is required: registering the scheduler is enough for the framework to activate retry behavior.

The built-in AsyncRetryScheduler executes retries on a ScheduledExecutorService according to a RetryPolicy. Axon Framework provides three composable RetryPolicy implementations:

ExponentialBackOffRetryPolicy

Doubles the retry delay after each attempt, starting from a given initial wait time in milliseconds. Retries indefinitely on its own, so combine it with MaxAttemptsPolicy to cap the number of attempts.

// Retry with delays of 100ms, 200ms, 400ms, 800ms, ...
RetryPolicy policy = new ExponentialBackOffRetryPolicy(100);
MaxAttemptsPolicy

Wraps any other policy and stops retrying once the maximum number of attempts is reached.

// Retry at most 3 times using the given delegate policy
RetryPolicy policy = new MaxAttemptsPolicy(delegate, 3);
FilteringRetryPolicy

Wraps any other policy and only delegates to it when the latest exception matches a given predicate. Failures that do not match are propagated immediately without retrying.

// Only retry when the failure is not a validation error
RetryPolicy policy = new FilteringRetryPolicy(
        delegate,
        e -> !(e instanceof IllegalArgumentException)
);

Compose them to express the exact behavior you need. A common combination is exponential backoff, capped at a maximum number of retries, filtered to transient errors only:

RetryPolicy policy = new MaxAttemptsPolicy(
        new FilteringRetryPolicy(
                new ExponentialBackOffRetryPolicy(100),
                e -> !(e instanceof IllegalArgumentException)
        ),
        3
);

Register an AsyncRetryScheduler with the policy to enable automatic retry behavior:

  • Configuration API

  • Spring Boot

import org.axonframework.messaging.core.configuration.MessagingConfigurer;
import org.axonframework.messaging.core.retry.AsyncRetryScheduler;
import org.axonframework.messaging.core.retry.ExponentialBackOffRetryPolicy;
import org.axonframework.messaging.core.retry.FilteringRetryPolicy;
import org.axonframework.messaging.core.retry.MaxAttemptsPolicy;
import org.axonframework.messaging.core.retry.RetryScheduler;

import java.util.concurrent.Executors;

public class AxonConfig {

    public void configureRetryScheduler(MessagingConfigurer configurer) {
        configurer.componentRegistry(cr -> cr.registerComponent(
                RetryScheduler.class,
                config -> new AsyncRetryScheduler(
                        new MaxAttemptsPolicy(
                                new FilteringRetryPolicy(
                                        new ExponentialBackOffRetryPolicy(100),
                                        e -> !(e instanceof IllegalArgumentException)
                                ),
                                3
                        ),
                        Executors.newSingleThreadScheduledExecutor()
                )
        ));
    }
}
import org.axonframework.messaging.core.retry.AsyncRetryScheduler;
import org.axonframework.messaging.core.retry.ExponentialBackOffRetryPolicy;
import org.axonframework.messaging.core.retry.FilteringRetryPolicy;
import org.axonframework.messaging.core.retry.MaxAttemptsPolicy;
import org.axonframework.messaging.core.retry.RetryScheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;

@Configuration
public class AxonConfig {

    @Bean
    public RetryScheduler retryScheduler() {
        return new AsyncRetryScheduler(
                new MaxAttemptsPolicy(
                        new FilteringRetryPolicy(
                                new ExponentialBackOffRetryPolicy(100),
                                e -> !(e instanceof IllegalArgumentException)
                        ),
                        3
                ),
                Executors.newSingleThreadScheduledExecutor()
        );
    }
}

When the built-in policies do not cover your requirements, implement RetryPolicy directly. The interface receives the failed message, the current exception, and the exception types from all previous attempts, and returns a RetryPolicy.Outcome that either schedules the next attempt or aborts:

import org.axonframework.common.infra.ComponentDescriptor;
import org.axonframework.messaging.core.Message;
import org.axonframework.messaging.core.retry.RetryPolicy;

import java.util.List;
import java.util.concurrent.TimeUnit;

public class CustomRetryPolicy implements RetryPolicy {

    @Override
    public Outcome defineFor(Message message, Throwable failure,
                             List<Class<? extends Throwable>[]> previousFailures) {
        if (failure instanceof IllegalArgumentException || previousFailures.size() >= 3) {
            return Outcome.doNotReschedule();
        }
        return Outcome.rescheduleIn(2, TimeUnit.SECONDS);
    }

    @Override
    public void describeTo(ComponentDescriptor descriptor) {
        descriptor.describeProperty("maxRetries", 3);
        descriptor.describeProperty("retryDelay", "2 SECONDS");
    }
}

Command gateway

The command gateway provides a convenient, type-safe API for dispatching commands. While you can dispatch commands directly through the command bus, the gateway is generally the easiest option.

Using the default CommandGateway

The CommandGateway interface provides several methods for sending commands:

public interface CommandGateway {
    // Send and get CommandResult for full control
    CommandResult send(@Nonnull Object command);

    // Send and get CompletableFuture with converted result type
    CompletableFuture<R> send(@Nonnull Object command, @Nonnull Class<R> resultType);

    // Send and wait for result
    Object sendAndWait(@Nonnull Object command);
}

Example usage:

@RestController
public class OrderController {

    private final CommandGateway commandGateway;

    public OrderController(CommandGateway commandGateway) {
        this.commandGateway = commandGateway;
    }

    @PostMapping("/orders")
    public CompletableFuture<String> createOrder(@RequestBody CreateOrderRequest request) {
        CreateOrderCommand command = new CreateOrderCommand(
            UUID.randomUUID().toString(),
            request.getProductId(),
            request.getQuantity()
        );

        // Returns CompletableFuture<String> with the order ID
        return commandGateway.send(command, String.class);
    }

    @PostMapping("/orders/{id}/ship")
    public void shipOrder(@PathVariable String id) {
        // Fire and forget
        commandGateway.send(new ShipOrderCommand(id));
    }
}

Axon Framework will construct a CommandGateway component for you out of the box at all times. With Axon’s Spring Boot integration, you can thus automatically inject a CommandGateway bean when needed.

Command handling component registration

Command handlers need to be registered with the framework so commands can be routed to them.

Spring Boot auto-registration

With Spring Boot, command handlers are automatically discovered and registered when you:

  • Use @Component or @Service on handler classes

  • Use @EventSourced on entity classes with command handlers

@Component
public class OrderCommandHandler {

    @CommandHandler
    public void handle(CreateOrderCommand command) {
        // Handler implementation
    }
}
@EventSourced
public class Order {

    @CommandHandler
    public static Order handle(CreateOrderCommand command) {
        // Creation handler
        return new Order(command.orderId(), command.productId());
    }
}

Manual registration with configuration API

When not using Spring Boot, register command handlers using CommandHandlingModule:

import org.axonframework.messaging.core.configuration.MessagingConfigurer;
import org.axonframework.messaging.commandhandling.configuration.CommandHandlingModule;

public class AxonConfig {

    public void configureHandlers() {
        MessagingConfigurer.create()
            .registerCommandHandlingModule(
                CommandHandlingModule.named("order-commands")
                    .commandHandlers(handlers ->
                        handlers.autodetectedCommandHandlingComponent(
                            config -> new OrderCommandHandler(
                                config.getComponent(Repository.class)
                            )
                        )
                    )
            );
    }
}

The autodetectedCommandHandlingComponent() method scans the handler for @CommandHandler annotated methods and registers them with the command bus. When you prefer to register methods as command handlers without the use of annotations, use the commandHandler(QualifiedName, CommandHandler) method instead.

For registering event-sourced entities and their repositories, use EventSourcingConfigurer or ModellingConfigurer which provide entity-specific registration methods.

Distributed command bus

For applications that need to distribute command handling across multiple JVM instances, Axon provides distributed command bus implementations. To that end, Axon Framework has a special type of CommandBus implementation, called the DistributedCommandBus.

The DistributedCommandBus takes in a local CommandBus (called the localSegment), a CommandBusConnector, and DistributedCommandBusConfiguration:

import org.axonframework.messaging.core.configuration.MessagingConfigurer;
import org.axonframework.messaging.commandhandling.SimpleCommandBus;
import io.axoniq.framework.messaging.commandhandling.distributed.CommandBusConnector;
import io.axoniq.framework.messaging.commandhandling.distributed.DistributedCommandBus;
import io.axoniq.framework.messaging.commandhandling.distributed.DistributedCommandBusConfiguration;
import org.axonframework.messaging.core.unitofwork.UnitOfWorkFactory;

public class AxonConfig {

    public void configureDistributedCommandBus(MessagingConfigurer configurer) {
        configurer.registerCommandBus(
                config -> {
                    SimpleCommandBus localSegment = new SimpleCommandBus(
                            config.getComponent(UnitOfWorkFactory.class)
                    );
                    return new DistributedCommandBus(
                            localSegment,
                            config.getComponent(CommandBusConnector.class),
                            DistributedCommandBusConfiguration.DEFAULT
                    );
                }
        );
    }
}

The local segment covers command handler registration and command handler invocations within a given JVM instance. Hence, it covers the local activities of command processing. The CommandBusConnector is in charge of dispatching and receiving commands to and from other JVM. The CommandBusConnector has several implementation, as described further below. The DistributedCommandBusConfiguration allows customization of load factor, threads, and executor used by the DistributedCommandBus.

Local handler shortcut

A node can prefer its own command handler for selected commands instead of routing every command through the connector. Register a LocalCommandDispatchPredicate to enable this. It is consulted for each outgoing dispatch and receives the CommandMessage and the (nullable) ProcessingContext, so the decision can be based on the command’s payload, type, metadata, or a flag placed on the context by the dispatcher.

The shortcut only takes effect when the local segment actually subscribed a handler for the command. When it did not, the command is routed through the connector as usual, regardless of the predicate. This ensures a locally preferred command that this node cannot handle is still routed to a segment that can.

Short-cut commands are handled on the same bounded, priority-ordered worker pool as commands arriving from remote segments, because the shortcut reuses the same local-handling path. A burst of locally preferred commands is therefore subject to the same back-pressure and cannot flood the local segment beyond what it would already accept from remote traffic.

import io.axoniq.framework.messaging.commandhandling.distributed.LocalCommandDispatchPredicate;
import org.axonframework.messaging.core.configuration.MessagingConfigurer;

public class AxonConfig {
    public void configureLocalCommandShortcut(MessagingConfigurer configurer) {
        configurer.componentRegistry(registry -> registry.registerComponent(
                LocalCommandDispatchPredicate.class,
                config -> (command, context) -> true   // prefer the local handler when subscribed
        ));
    }
}

The shortcut is opt-in: without a LocalCommandDispatchPredicate, commands are always routed through the connector, leaving the default distributed behavior unchanged.

AxonServerCommandBusConnector

The AxonServerCommandBusConnector connects to Axon Server for distributed command routing. This is the default when using the Axon Spring Boot starter with axon-server-connector dependency.

With Spring Boot:

<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>${axon.version}</version>
</dependency>

Configure connection in application.properties:

axon.axonserver.servers=localhost:8124
axon.axonserver.enabled=true

To disable Axon Server and use local command bus instead:

axon.axonserver.enabled=false

SpringCloudCommandBusConnector

The SpringCloudCommandBusConnector distributes commands over HTTP between the application instances a Spring Cloud DiscoveryClient reports, without an Axon Server in between. It suits deployments that already run a service registry, such as Eureka, Consul, or Kubernetes, and would rather route commands through it than add another component. Commands with the same routing key always go to the same instance, chosen from the instances that report handling that command.

The connector calls the instances it routes between members, the word its API, properties, and log messages use.

Adding the connector

Add the starter alongside a discovery implementation of your choosing:

<dependency>
    <groupId>io.axoniq.framework</groupId>
    <artifactId>axoniq-springcloud</artifactId>
    <version>${axoniq-framework.version}</version>
</dependency>
<dependency>
    <groupId>io.axoniq.framework</groupId>
    <artifactId>axoniq-spring-boot-starter</artifactId>
    <version>${axoniq-framework.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Axoniq Framework does not manage the versions of the discovery implementations, so import the Spring Cloud release train matching your Spring Boot version:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

The starter brings the connector, its autoconfiguration, and the servlet web stack commands travel over. The discovery implementation stays your choice, and is what supplies the DiscoveryClient and Registration the connector needs. No configuration of the connector itself is needed.

An application that adds the starter without a discovery implementation says so on start-up rather than starting with commands that are never distributed. The same holds for an application that could not serve the endpoints other members reach it on: one without a web stack, or one running on WebFlux. Set axon.springcloud.enabled=false to handle commands locally instead.

The same starter also carries the SpringCloudQueryBusConnector, which distributes queries over the same discovery and the same ring.

Applications preferring to wire these beans themselves can depend on axoniq-springcloud alone. Spring itself is still required: the connector learns about the cluster from Spring Cloud’s discovery events, and receives messages and capability requests through Spring MVC controllers. Each Spring Cloud component Axoniq Framework supports has a module of its own, so an application takes only the ones it uses.

Distributing commands through Spring Cloud and through Axon Server are alternatives: an application uses one or the other, not both. Only one connector distributes commands, and an explicitly configured one wins, so make the choice explicit: set axon.axonserver.enabled=false to distribute commands through Spring Cloud, or axon.springcloud.enabled=false to distribute them through Axon Server. An application that leaves both enabled distributes commands through Axon Server.

Command throughput is claimed against your Axoniq license. Without Axon Server there to supply it, the license comes from Axoniq Platform, or from a license file the application instances read themselves. Whether a throughput limit applies is a property of the license itself rather than of where it comes from, and not every license carries one. See Feature entitlement and message throughput for how a limit is enforced when a license carries one.

Reaching the other members

Members reach each other over three paths, which have to be the same on every member of a cluster:

axon.springcloud.command-endpoint=/axoniq-springcloud/command
axon.springcloud.query-endpoint=/axoniq-springcloud/query
axon.springcloud.capabilities-endpoint=/axoniq-springcloud/member-capabilities

All three are served on the application’s own HTTP port, alongside its other endpoints, and carry traffic between members only. Keep them off any ingress reachable from outside the cluster. An application with Spring Security in place has to permit all three paths and exempt the command and query endpoints from cross-site request forgery protection, or members cannot reach each other.

Outgoing requests to other members use the application’s own RestClient.Builder bean when it has one, which is where an interceptor authenticating them belongs. Spring Boot contributes such a builder by default, so the application’s spring.http.client.* settings apply to this traffic as well. Define a RestClient bean named axoniqSpringCloudRestClient to give this traffic a client of its own. An application whose RestClient.Builder is load balanced has to do so: members are addressed by host and port rather than by service ID, which a load-balanced client cannot resolve.

Instances that are not served from the root publish their context root as service instance metadata, and tell the connector which metadata property holds it:

eureka.instance.metadata-map.contextRoot=/orders
axon.springcloud.context-root-metadata-property-name=contextRoot

Keeping up with the cluster

Members ask each other what they handle once per discovery heartbeat, which makes the heartbeat interval the speed at which a change reaches the rest of the cluster. The heartbeats come from the discovery implementation: Eureka publishes one on every registry fetch, eureka.client.registry-fetch-interval-seconds, which is 30 seconds by default. A deployment whose discovery implementation publishes no heartbeats can drive a round itself, by calling updateMemberships() on the SpringCloudMemberRegistry bean.

A command no member reports handling fails with a NoHandlerForCommandException, which is also what a dispatch sees for up to one heartbeat after the last member handling that command has gone away. A member that does not answer within axon.springcloud.command-reply-timeout fails the command it was sent and is taken out of the ring, so the commands behind it go elsewhere until a later heartbeat finds it again. A member that answers with a failure of its own, such as a handler that threw, stays in the ring, since it was reachable.

Narrowing which instances are considered

A discovery registry reports every service registered with it, not only those running this connector. Each instance it reports is asked for its capabilities, and one that answers with a client error is left alone for axon.springcloud.ignore-period before being asked again.

On a registry holding many services, contribute a Predicate<ServiceInstance> bean to decide which instances are considered at all. Instances it rejects are never asked, which is cheaper than discovering them and ignoring them one heartbeat at a time.

@Bean
public Predicate<ServiceInstance> axoniqSpringCloudInstanceFilter() {
    return instance -> "orders".equals(instance.getServiceId());
}

The predicate has to accept this application’s own instance as well, since it is a member of the ring like any other.

Properties

The three endpoint paths above are the properties a cluster has to agree on. The rest rarely need changing:

# Set to false to distribute messages some other way, or not at all.
axon.springcloud.enabled=true
# How long a member is given to answer a command before it is treated as unreachable.
axon.springcloud.command-reply-timeout=30s
# How long an instance is given to answer a capabilities request. Keep it well under the heartbeat interval.
axon.springcloud.capabilities-timeout=2s
# How long a service instance is left alone after answering the capabilities endpoint with a client error.
axon.springcloud.ignore-period=1m
# The metadata property holding an instance's context root. Unset by default, for instances served from the root.
#axon.springcloud.context-root-metadata-property-name=contextRoot

The properties governing how queries are answered are described with the query connector.

The command load factor is not among them: it belongs to the distributed command bus as a whole, and is set through the DistributedCommandBusConfiguration the DistributedCommandBus is built with. A member with twice the load factor of another receives roughly twice the commands.

Routing strategy

Distributed command buses use the routing information in every CommandMessage to decide how to route your commands. The CommandGateway is typically in charge of setting the CommandMesasge#routingKey field, as it constructs the CommandMessage for you. As such, the RoutingStrategy is a component present on the DefaultCommandGateway. Commands with the same routing key always go to the same instance.

If your node topology is instable, routing of commands with the same key will no longer be consistent.

The default AnnotationRoutingStrategy uses the routingKey attribute on the @Command annotation:

@Command(routingKey = "orderId")
public class ShipOrderCommand {

    private final String orderId;
    private final String trackingNumber;

    // constructor, getters
}

The routingKey attribute specifies which property of the command should be used for routing. Commands with the same routing key value will be routed to the same instance.

It’s recommended to annotate all command classes with @Command to explicitly declare their message properties, including namespace, name, version, and routing key. This makes your commands self-documenting and ensures consistent message identification across your application.

Automatic command sequencing: The command bus automatically includes a sequencing interceptor that prevents concurrent commands from causing optimistic locking failures. It defaults to using the routing key as sequencing identifier, processing commands targeting the same routing key sequentially while maintaining full concurrency for different routing keys.

For more details and customization options, see Command sequencing.

Custom routing strategies can be configured:

  • Configuration API

  • Spring Boot

import org.axonframework.messaging.core.configuration.MessagingConfigurer;

public class AxonConfig {

    public void configureRoutingStrategy() {
        MessagingConfigurer configurer = MessagingConfigurer.create();

        configurer.componentRegistry(registry ->
            registry.registerComponent(
                RoutingStrategy.class,
                config -> new AnnotationRoutingStrategy()
            )
        );
    }
}
@Configuration
public class AxonConfig {

    @Bean
    public RoutingStrategy routingStrategy() {
        // Use default (Command.class annotation)
        return new AnnotationRoutingStrategy();
    }
}

You can also use MetadataRoutingStrategy to route based on metadata values:

@Configuration
public class AxonConfig {

    @Bean
    public RoutingStrategy routingStrategy() {
        return new MetadataRoutingStrategy("routingKey");
    }
}