Spring Boot Integration
The Axon Framework Spring Boot extension maps Axon’s configuration API onto the Spring Application Context. It provides auto-configuration so that, in most cases, adding the starter dependency is all that is needed to get a fully configured Axon application. This page explains how the Spring integration builds on the same concepts described in the rest of this chapter.
For setup instructions, Spring Boot version support, and event processor-specific configuration, see Spring Boot integration.
How Spring uses the configuration API
The Spring integration does not bypass the configuration API: it implements it.
In practice, this means the Spring ApplicationContext and Axon’s Configuration share the same set of components: any infrastructure component Axon provides (the CommandBus, EventStore, QueryBus, and so on) is automatically made available as a Spring bean, injectable like any other bean.
The reverse also holds.
If you declare your own @Bean of a matching infrastructure type, Axon picks it up and uses it instead of its own default, no further wiring required:
import org.axonframework.messaging.commandhandling.CommandBus;
import org.axonframework.messaging.commandhandling.SimpleCommandBus;
import org.axonframework.messaging.core.unitofwork.UnitOfWorkFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AxonConfig {
@Bean
public CommandBus commandBus(UnitOfWorkFactory unitOfWorkFactory) {
return new SimpleCommandBus(unitOfWorkFactory);
}
}
You rarely need to register message handlers or entities yourself either.
Methods annotated with @CommandHandler, @EventHandler, or @QueryHandler on any Spring bean, and classes annotated with @EventSourced, are discovered automatically and registered as Modules, out of the box.
See Component detection and Modules for the details.
Customizing Axon’s configuration through Spring
The auto-configuration only registers components that have not been defined explicitly.
To replace a default component, define a @Bean of the matching type in any @Configuration class:
import org.axonframework.messaging.core.unitofwork.UnitOfWorkFactory;
import org.axonframework.messaging.queryhandling.QueryBus;
import org.axonframework.messaging.queryhandling.SimpleQueryBus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ReplaceQueryBusExample {
@Bean
public QueryBus queryBus(UnitOfWorkFactory unitOfWorkFactory) {
return new SimpleQueryBus(unitOfWorkFactory);
}
}
Spring’s standard bean override mechanism means the default QueryBus provided by Axon Framework is never created.
Thus, your bean becomes the one Axon uses instead.
To decorate a component, declare a DecoratorDefinition as a Spring bean.
The SpringComponentRegistry discovers all DecoratorDefinition beans in the Application Context automatically and registers them, so no manual componentRegistry access is needed:
import org.axonframework.common.configuration.DecoratorDefinition;
import org.axonframework.messaging.commandhandling.CommandBus;
import org.axonframework.messaging.tracing.SpanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TracingConfig {
@Bean
public DecoratorDefinition<CommandBus, TracingCommandBusDecorator> tracingCommandBusDecorator() {
return DecoratorDefinition.forType(CommandBus.class)
.with((config, name, delegate) -> new TracingCommandBusDecorator(
delegate, config.getComponent(SpanFactory.class)
))
.order(10);
}
}
For more expanded configuration integration, we recommend registering a ConfigurationEnhancer as a Spring bean instead.
The Spring integration discovers all ConfigurationEnhancer beans in the Application Context automatically and applies them during configuration.
The example below expands on the tracing-decoration example, but now through a ConfigurationEnhancer:
import org.axonframework.common.configuration.ComponentRegistry;
import org.axonframework.common.configuration.ConfigurationEnhancer;
import org.axonframework.messaging.commandhandling.CommandBus;
import org.axonframework.messaging.tracing.SpanFactory;
import org.springframework.stereotype.Component;
@Component
public class TracingEnhancer implements ConfigurationEnhancer {
@Override
public void enhance(ComponentRegistry registry) {
if (registry.hasComponent(CommandBus.class)) {
registry.registerDecorator(
CommandBus.class,
10,
(config, name, delegate) -> new TracingCommandBusDecorator(
delegate, config.getComponent(SpanFactory.class)
)
);
}
}
}
Message handler and entity detection
The Spring Boot Starter automatically detects Axon components in the Application Context:
-
Methods annotated with
@CommandHandler,@EventHandler, or@QueryHandleron any Spring bean are registered as message handlers with the appropriate bus. -
Classes annotated with
@EventSourcedare registered as event-sourced entities.
A single @Component can combine all three handler types; Axon discovers each annotated method independently:
import org.axonframework.messaging.commandhandling.annotation.CommandHandler;
import org.axonframework.messaging.eventhandling.annotation.EventHandler;
import org.axonframework.messaging.eventhandling.gateway.EventAppender;
import org.axonframework.messaging.queryhandling.annotation.QueryHandler;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* We have combined a command, event, and query handler in one class as an example only. Typically, these handlers are
* separated.
*/
@Component
public class GiftCardHandler {
private final Map<String, Integer> balances = new ConcurrentHashMap<>();
@CommandHandler
public void handle(RedeemGiftCardCommand command, EventAppender eventAppender) {
eventAppender.append(new GiftCardRedeemedEvent(command.cardId(), command.amount()));
}
@EventHandler
public void on(GiftCardRedeemedEvent event) {
balances.merge(event.cardId(), -event.amount(), Integer::sum);
}
@QueryHandler
public GiftCardBalance handle(FetchGiftCardBalanceQuery query) {
return new GiftCardBalance(query.cardId(), balances.getOrDefault(query.cardId(), 0));
}
}
Handler discovery happens through MessageHandlerConfigurer and MessageHandlerLookup, which scan the bean factory for beans implementing CommandHandlingComponent, EventHandlingComponent, or QueryHandlingComponent, as well as beans with annotated handler methods.
Discovered handlers are wrapped into a CommandHandlingModule and QueryHandlingModule, and discovered @EventSourced entities into an EventSourcedEntityModule, each registered on the ComponentRegistry the same way a manually built Module would be.
Axon properties
Besides beans, decorators, and enhancers, several parts of the auto-configuration can be tuned directly through application.properties or application.yml, without writing any code:
| Property | What it does |
|---|---|
|
Enables or disables the Axon Server connector integration. Defaults to |
|
Configures an individual event processor: mode, initial segment count, batch size, thread count, and more. See Event processor configuration below for the full reference. |
|
How long a streaming event processor waits before forcing a claim on a token segment that has not been updated. |
|
Tuning for the JPA-based event storage engine: batch size, gap cleaning threshold, gap timeout, and polling interval. |
|
Selects the |
|
Enables and tunes timeouts for transactions and message handlers (commands, events, queries, and deadlines). |
|
Enables and tunes OpenTelemetry-based tracing for Axon’s infrastructure components. See Tracing for the full property reference. |
|
Disables Axon’s startup update-check ping, or points it at a different URL. |
Event processor configuration
Spring Boot provides two approaches to configure event processors: properties-based configuration, covered as part of getting started in Event processor configuration, and programmatic, type-safe configuration through EventProcessorDefinition beans, covered below.
Reach for properties-based configuration when the configuration is simple and doesn’t require custom logic, settings vary per environment, or you’re still prototyping. A simple property-based setup looks as follows:
axon.eventhandling.processors.my-processor.mode=pooled-streaming
axon.eventhandling.processors.my-processor.initial-segment-count=4
Reach for EventProcessorDefinition beans instead when you need fine-grained control over handler assignment, the configuration requires conditional logic, type safety and IDE support matter, or you want to share configuration between environments programmatically.
Here is what such a bean looks like:
import org.axonframework.extension.spring.config.EventProcessorDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EventProcessorConfig {
@Bean
public EventProcessorDefinition orderProcessor() {
return EventProcessorDefinition.pooledStreaming("order-processor")
.assigningHandlers(descriptor ->
descriptor.beanName().startsWith("order"))
.customized(config -> config
.initialSegmentCount(4)
.batchSize(100));
}
}
The EventProcessorDefinition API provides:
-
Type-safe configuration - Compile-time checking of configuration options
-
Fluent API - Chain configuration methods for readability
-
Handler selection - Define which event handlers belong to each processor using predicates
-
Full configuration access - All processor settings available programmatically
-
IDE support - Better autocompletion and refactoring support
Handler selection
The assigningHandlers() method provides access to an EventHandlerDescriptor with the following properties:
-
beanName()- The Spring bean name -
beanType()- The event handler class (use for package-based selection) -
beanDefinition()- The Spring bean definition -
component()- The component builder
This allows flexible handler assignment based on naming conventions, package structure, or custom criteria:
import org.axonframework.extension.spring.config.EventProcessorDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class HandlerSelectionExamples {
@Bean
public EventProcessorDefinition byBeanNamePrefix() {
// By bean name prefix
return EventProcessorDefinition.pooledStreaming("order-processor")
.assigningHandlers(d -> d.beanName().startsWith("order"))
.notCustomized();
}
@Bean
public EventProcessorDefinition byPackage() {
// By package
return EventProcessorDefinition.pooledStreaming("orders-package-processor")
.assigningHandlers(d -> d.beanType().getPackageName().startsWith("com.example.orders"))
.notCustomized();
}
@Bean
public EventProcessorDefinition byBeanNamePattern() {
// By bean name pattern
return EventProcessorDefinition.pooledStreaming("handler-processor")
.assigningHandlers(d -> d.beanName().contains("Handler"))
.notCustomized();
}
}
For easy matching of Event Handling Components to Event Processors purely based on the Event Processor name, you can use the @Namespace annotation in combination with the EventHandlerSelector.matchesNamespaceOnType().
The @Namespace annotation can be placed on the Event Handling Component, where the value of the annotation determines the Event Processor name it belongs to.
Handlers will be automatically matched when defining an EventProcessorDefinition using the EventHandlerSelector.matchesNamespaceOnType() with the same name as defined in the @Namespace.
Let’s take a look at a concrete example:
import org.axonframework.extension.spring.config.EventHandlerSelector;
import org.axonframework.extension.spring.config.EventProcessorDefinition;
@Bean
public EventProcessorDefinition ordersProcessor() {
return EventProcessorDefinition.pooledStreaming("orders")
.assigningHandlers(EventHandlerSelector.matchesNamespaceOnType("orders"))
.notCustomized();
}
@Namespace("orders")
public class OrderEventHandler {
// omitted event handlers for brevity
}
With @Namespace on the OrderEventHandler, the OrderEventHandler signals it belongs to the Event Processor named orders.
Note that the @Namespace annotation can be placed on a class (as shown above), on an enclosing class, on a package-info.java file, or on a module-info.java file.
The @Namespace annotation is searched for in the order defined above.
-
On the type
-
On the enclosing class
-
On the
package-info.javafile -
On the
module-info.javafile
This allows you to use @Namespace to describe multiple event handlers belonging to a package, as shown below:
@Namespace("orders")
package com.example.orders;
import org.axonframework.messaging.core.annotation.Namespace;
|
Convenience
EventProcessorDefinition methodsFor the common case of matching handlers by namespace,
These methods use the processor name as the namespace to match, making it easy to organize processors by namespace or bounded context. |
Configuration options
Use customized() to customize processor settings, or notCustomized() when you only need handler assignment:
// With custom configuration
EventProcessorDefinition.pooledStreaming("custom-processor")
.assigningHandlers(descriptor -> descriptor.beanName().startsWith("custom"))
.customized(config -> config
.initialSegmentCount(16)
.batchSize(100)
.tokenClaimInterval(5000));
// With default settings (only handler assignment)
EventProcessorDefinition.pooledStreaming("default-processor")
.assigningHandlers(descriptor -> descriptor.beanName().startsWith("default"))
.notCustomized();
Configuration precedence
When using multiple configuration approaches:
-
EventProcessorDefinitionbeans take precedence over properties-based configuration -
If a handler matches multiple
EventProcessorDefinitionselectors, anAxonConfigurationExceptionis thrown at startup -
Handlers not matched by any
EventProcessorDefinitionare assigned based on their package name (default behavior)
To avoid conflicts, ensure each handler is matched by at most one EventProcessorDefinition using mutually exclusive selectors.
For comprehensive event processor configuration examples and detailed information, see Event Processors.
Implementation details
The classes below are what make the behavior above possible. You will not typically interact with them directly, but they are useful to know when diagnosing configuration issues:
SpringAxonApplication-
A Spring
@Componentthat implementsApplicationConfigurer. It holds aSpringComponentRegistryand aSpringLifecycleRegistryand delegates to them whencomponentRegistry(…)orlifecycleRegistry(…)are called. It is the Spring equivalent ofDefaultAxonApplicationused in plain Java setup. SpringComponentRegistry-
A Spring
BeanPostProcessorandBeanFactoryPostProcessorthat implementsComponentRegistry. It connects Axon’s component model to the Spring bean lifecycle. Registrations made throughregisterComponent,registerDecorator, andregisterEnhancerare all stored here and applied when Spring initializes beans. SpringLifecycleRegistry-
A
LifecycleRegistrybacked by Spring’sSmartLifecyclemechanism.onStarthandlers are invoked during Spring’s context startup phase;onShutdownhandlers are invoked during Spring’s context close phase.
Because SpringAxonApplication implements ApplicationConfigurer, you can use the same MessagingConfigurer, ModellingConfigurer, or EventSourcingConfigurer wrapping pattern in a Spring context.
The SpringAxonApplication bean is the root configurer that gets wrapped.
The SpringComponentRegistry makes the behavior described above possible in two ways.
As a BeanPostProcessor, it checks, right after Spring creates a bean, whether any registered decorator or enhancer targets that bean’s type; if so, the decorator is applied and the wrapped instance is used in place of the original, so the standard Axon decorator mechanism works seamlessly for Spring-managed beans.
As a BeanFactoryPostProcessor, it allows the resulting Configuration to look up any bean from the Spring ApplicationContext, not just those explicitly registered with the ComponentRegistry.
This is what makes the CommandBus bean shown above available to Axon: it is treated as the registered CommandBus component, and any decorator registered through the ComponentRegistry (or through a ConfigurationEnhancer) is applied on top of it.
Lifecycle integration
Spring’s SmartLifecycle drives Axon startup and shutdown when using the Spring Boot Starter.
The SpringLifecycleStartHandler and SpringLifecycleShutdownHandler integrate Axon’s lifecycle phases with Spring’s context lifecycle:
-
On context startup: Axon’s lifecycle phases run in ascending phase order.
-
On context shutdown: Axon’s lifecycle phases run in descending phase order (reverse startup order).
This means calling AxonConfiguration#start() is not required and has no effect in a Spring application.
The lifecycle is managed entirely by the Spring context.
Modules in Spring
Module implementations registered via componentRegistry work identically in Spring: the SpringComponentRegistry treats module registrations the same as in plain Java, preserving the same one-way visibility between a module and its parent.
Axon’s built-in modules (for example, the event processor modules registered through EventProcessorDefinition) use this mechanism to keep event processor infrastructure isolated from the application’s root configuration.