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);
}
}
Components registered through the ComponentRegistry, whether by Axon’s defaults or by a ConfigurationEnhancer, become Spring beans as follows:
-
A component registered by type only becomes a bean named after the fully qualified name of that type, for example
org.axonframework.messaging.commandhandling.CommandBus. Injecting the type yields this component, also when named components of the same type exist. -
A component registered under a name becomes a bean with that name. Inject it with
@Qualifiercarrying the component name. Without a qualifier, Spring only injects a named component when it is the only candidate for the type.
When Axon looks up a component by type and several beans of that type exist, it uses the @Primary bean or the bean named after the fully qualified name of the type.
Without either, the lookup throws an AmbiguousComponentMatchException, so the application fails to start when Axon needs that component during startup.
Properties that refer to a component by its name, such as the source and token-store settings of an event processor, use these bean names.
A lookup by name returns the bean with that name.
Without such a bean, it returns the single bean of the requested type that has the name as an alias or in its @Qualifier.
When several beans of that type have the name as their qualifier, the lookup throws an AmbiguousComponentMatchException.
import org.axonframework.common.configuration.ComponentRegistry;
import org.axonframework.common.configuration.ConfigurationEnhancer;
import org.axonframework.messaging.eventhandling.processing.streaming.token.store.TokenStore;
import org.axonframework.messaging.eventhandling.processing.streaming.token.store.inmemory.InMemoryTokenStore;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
class ReplayTokenStoreEnhancer implements ConfigurationEnhancer {
@Override
public void enhance(ComponentRegistry registry) {
registry.registerComponent(TokenStore.class, "replayTokenStore", config -> new InMemoryTokenStore());
}
}
@Component
class TokenStoreMaintenance {
private final TokenStore tokenStore;
private final TokenStore replayTokenStore;
TokenStoreMaintenance(TokenStore tokenStore, (1)
@Qualifier("replayTokenStore") TokenStore replayTokenStore) { (2)
this.tokenStore = tokenStore;
this.replayTokenStore = replayTokenStore;
}
}
| 1 | The default TokenStore, injected by type. |
| 2 | The TokenStore registered under the name replayTokenStore. |
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.
Transactions
When the application context contains a Spring PlatformTransactionManager together with a DataSource or an EntityManagerFactory, the Spring Boot Starter registers a SpringTransactionManager.
This lets Axon use the Spring transaction manager for the units of work it runs.
Who owns the transaction
Every unit of work (the processing context of a command, event, or query) starts a Spring transaction in its pre-invocation phase, commits it in its commit phase, and rolls it back when anything in the unit of work fails. Axon only commits and rolls back the transaction it started itself. A unit of work runs its phases on the thread that started it, as Spring ties a transaction to a thread.
Sharing the connection
Components that need a database connection or an entity manager, such as the token store of an event processor, use the one that belongs to the transaction of the unit of work.
For this to work, the application and Axon have to use the same DataSource (or EntityManagerFactory), which is the one the Spring transaction manager manages.
Your own code in a unit of work
Spring components that a handler calls, including methods annotated with @Transactional, take part in the transaction of the unit of work.
That includes checks that must be consistent with the work of the unit of work, such as inserting into a table with a unique constraint: when the insert fails, the unit of work fails and is rolled back as a whole, including the events it appended when the event store writes to the same database, as the JPA event store does.
Start units of work outside of transactions
Axon recommends not starting a transaction before the framework does.
A unit of work has to own its transaction, so it has to start on a thread that has no active transaction.
When a unit of work starts on a thread with an active transaction, and it would take part in that transaction, it fails with an IllegalTransactionStateException and the active transaction is left untouched.
The reason is that a unit of work that takes part in an active transaction does not own the commit. Its commit phase does not commit anything, so everything that runs after the commit runs before the data is committed: event stores that finalize or publish appended events after the commit act too early, which can leave events invisible to streams, or skipped by them. Subscription query updates, the callbacks of event processors, and monitoring then report work that is not committed yet. The unit of work also reports success while the active transaction can still roll its work back.
The usual way to end up here is a command that is handled on the thread that sends it, for example by the SimpleCommandBus, from a method that is annotated with @Transactional.
Send the command from a method that is not transactional instead, and keep other database work in separate transactional methods.
When that is not possible, register a TransactionManager bean that starts a new transaction for each unit of work, with the PROPAGATION_REQUIRES_NEW propagation:
new SpringTransactionManager(
transactionManager,
entityManagerProvider,
connectionProvider,
new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
);
The unit of work then runs in a new transaction while the active one is suspended. It needs a second connection while the active transaction holds its own, so size the connection pool for it, and it commits independently of the active transaction.
|
Setting |
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). |
|
Whether a unit of work that starts while a transaction is already active may take part in that transaction. Defaults to |
|
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.
Components registered with the ComponentRegistry become bean definitions when the registry initializes, just before Spring creates the first application bean.
A named component is registered as a fallback bean (AbstractBeanDefinition#setFallback).
When an unqualified injection point matches both the type-level component and named components, Spring selects the single non-fallback candidate: the type-level component.
A @Qualifier selects a named component by its bean name.
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.