Component Message Intercepting

Component-level interceptors apply cross-cutting behavior within a single handling component instance. They complement bus-level interceptors, which apply globally to all messages on a bus, by targeting only the handlers within a specific component. Axon Framework supports two approaches: annotated interceptor methods placed directly on the component class, and declarative interceptors registered during module configuration.

@CommandHandlerInterceptor annotation

Annotated interceptors are methods placed on a handling component class and marked with one of the interceptor annotations. The framework invokes these methods around the regular handler methods on the same component instance.

Two styles are supported:

  • Before-interceptor: a void method with no MessageHandlerInterceptorChain parameter. The framework runs the method before the handler and automatically proceeds the chain after the method returns normally. If the interceptor throws, the handler is not invoked.

  • Surround-interceptor: a method that returns MessageStream<?> and declares a MessageHandlerInterceptorChain<M> parameter. The method controls whether and when to call chain.proceed(message, context). It can short-circuit processing entirely by returning without calling proceed, or by returning MessageStream.failed(…​).

@CommandHandlerInterceptor

The @CommandHandlerInterceptor annotation marks a method as an interceptor for command handlers on the same component. It is a specialization of @MessageHandlerInterceptor scoped to CommandMessage.

Before-interceptor (logging all commands):

import org.axonframework.messaging.commandhandling.annotation.CommandHandlerInterceptor;
import org.axonframework.messaging.commandhandling.annotation.CommandHandler;

public class OrderCommandHandler {

    @CommandHandlerInterceptor
    void logCommand(CommandMessage command) {
        log.info("Handling command: {}", command.qualifiedName());
    }

    @CommandHandler
    void handle(PlaceOrderCommand command, ProcessingContext context) {
        // Handle the command
    }
}

Surround-interceptor (authorization check):

import org.axonframework.messaging.commandhandling.annotation.CommandHandlerInterceptor;
import org.axonframework.messaging.core.MessageHandlerInterceptorChain;
import org.axonframework.messaging.core.MessageStream;

public class OrderCommandHandler {

    @CommandHandlerInterceptor
    MessageStream<?> authorize(
            CommandMessage command,
            MessageHandlerInterceptorChain<CommandMessage> chain,
            ProcessingContext context
    ) {
        if (!securityContext.isAuthorized(command)) {
            return MessageStream.failed(new AccessDeniedException("Not authorized"));
        }
        return chain.proceed(command, context);
    }

    @CommandHandler
    void handle(PlaceOrderCommand command, ProcessingContext context) {
        // Handle the command
    }
}

@EventHandlerInterceptor

The @EventHandlerInterceptor annotation marks a method as an interceptor for event handlers on the same component. It is a specialization of @MessageHandlerInterceptor scoped to EventMessage.

Before-interceptor (tracing):

import org.axonframework.messaging.eventhandling.annotation.EventHandlerInterceptor;
import org.axonframework.messaging.eventhandling.annotation.EventHandler;

public class CardSummaryProjection {

    @EventHandlerInterceptor
    void trace(EventMessage event) {
        tracer.startSpan(event.qualifiedName());
    }

    @EventHandler
    void on(CardIssuedEvent event, ProcessingContext context) {
        // Handle event
    }
}

Surround-interceptor (conditional processing):

import org.axonframework.messaging.eventhandling.annotation.EventHandlerInterceptor;
import org.axonframework.messaging.core.MessageHandlerInterceptorChain;
import org.axonframework.messaging.core.MessageStream;

public class CardSummaryProjection {

    @EventHandlerInterceptor
    MessageStream<?> filterByTenant(
            EventMessage event,
            MessageHandlerInterceptorChain<EventMessage> chain,
            ProcessingContext context
    ) {
        if (!tenantId.equals(event.metadata().get("tenantId"))) {
            return MessageStream.empty();
        }
        return chain.proceed(event, context);
    }

    @EventHandler
    void on(CardIssuedEvent event, ProcessingContext context) {
        // Handle event
    }
}

@QueryHandlerInterceptor

The @QueryHandlerInterceptor annotation marks a method as an interceptor for query handlers on the same component. It is a specialization of @MessageHandlerInterceptor scoped to QueryMessage.

Before-interceptor (access logging):

import org.axonframework.messaging.queryhandling.annotation.QueryHandlerInterceptor;
import org.axonframework.messaging.queryhandling.annotation.QueryHandler;

public class CourseQueryHandler {

    @QueryHandlerInterceptor
    void auditQuery(QueryMessage query) {
        auditLog.record(query.qualifiedName(), securityContext.currentUser());
    }

    @QueryHandler
    CourseView handle(FindCourseQuery query, ProcessingContext context) {
        // Handle the query
        return null;
    }
}

Surround-interceptor (tenant authorization):

import org.axonframework.messaging.queryhandling.annotation.QueryHandlerInterceptor;
import org.axonframework.messaging.core.MessageHandlerInterceptorChain;
import org.axonframework.messaging.core.MessageStream;

public class CourseQueryHandler {

    @QueryHandlerInterceptor
    MessageStream<?> checkTenant(
            QueryMessage query,
            MessageHandlerInterceptorChain<QueryMessage> chain,
            ProcessingContext context
    ) {
        if (!tenantId.equals(query.metadata().get("tenantId"))) {
            return MessageStream.failed(new AccessDeniedException("Wrong tenant"));
        }
        return chain.proceed(query, context);
    }

    @QueryHandler
    CourseView handle(FindCourseQuery query, ProcessingContext context) {
        // Handle the query
        return null;
    }
}

Generic @MessageHandlerInterceptor

The base @MessageHandlerInterceptor annotation is available when you need to cover multiple message types or want to use the annotation directly. Specify the messageType attribute to limit the interceptor to a particular message type. Omitting messageType defaults to matching any Message.

Intercepting all messages on a component:

Omitting messageType matches any message, so the interceptor runs before every handler on the component, including event handlers and query handlers. This is useful for cross-cutting concerns such as logging or security checks that should apply regardless of the message type being processed.

import org.axonframework.messaging.core.Message;
import org.axonframework.messaging.core.interception.annotation.MessageHandlerInterceptor;
import org.axonframework.messaging.eventhandling.annotation.EventHandler;
import org.axonframework.messaging.queryhandling.annotation.QueryHandler;

public class CardSummaryProjection {

    @MessageHandlerInterceptor
    void interceptAll(Message message) {
        // Runs before every @EventHandler and @QueryHandler on this component
    }

    @EventHandler
    void on(CardIssuedEvent event, ProcessingContext context) {
        // Handle event
    }

    @QueryHandler
    CardSummaryView handle(FindCardSummaryQuery query, ProcessingContext context) {
        // Handle query
    }
}

Surround-interceptor using the base annotation:

import org.axonframework.messaging.core.interception.annotation.MessageHandlerInterceptor;
import org.axonframework.messaging.core.MessageHandlerInterceptorChain;
import org.axonframework.messaging.core.MessageStream;
import org.axonframework.messaging.queryhandling.QueryMessage;

public class CourseQueryHandler {

    @MessageHandlerInterceptor(messageType = QueryMessage.class)
    MessageStream<?> intercept(
            QueryMessage query,
            MessageHandlerInterceptorChain<QueryMessage> chain,
            ProcessingContext context
    ) {
        // Logic before handling
        MessageStream<?> result = chain.proceed(query, context);
        // Logic after handling (if needed, chain the stream)
        return result;
    }
}

Declarative interceptors

Declarative interceptors are registered at module configuration time rather than by annotating methods on the component class. This is useful when an interceptor is implemented as a standalone class, when the same interceptor must apply uniformly across an entire module, or when you want to keep cross-cutting logic separate from handler classes.

Each module configuration API provides an intercepted method that accepts a ComponentBuilder for the interceptor. Multiple calls to intercepted accumulate interceptors in registration order.

Command interceptors

Use CommandHandlingModule.CommandHandlerPhase.intercepted() to register an interceptor for the command handling component assembled by a module.

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

CommandHandlingModule.named("orders")
        .commandHandlers()
        .autodetectedCommandHandlingComponent(cfg -> new OrderCommandHandler())
        .intercepted(cfg -> (message, context, chain) -> {
            log.info("Intercepting command: {}", message.qualifiedName());
            return chain.proceed(message, context);
        })
        .build();

Multiple interceptors are applied in registration order:

CommandHandlingModule.named("orders")
        .commandHandlers()
        .autodetectedCommandHandlingComponent(cfg -> new OrderCommandHandler())
        .intercepted(cfg -> new AuthorizationCommandInterceptor(cfg.getComponent(SecurityContext.class)))
        .intercepted(cfg -> new AuditCommandInterceptor(cfg.getComponent(AuditLog.class)))
        .build();

Query interceptors

Use QueryHandlingModule.QueryHandlerPhase.intercepted() to register an interceptor for the query handling component assembled by a module.

import org.axonframework.messaging.queryhandling.configuration.QueryHandlingModule;

QueryHandlingModule.named("courses")
        .queryHandlers()
        .autodetectedQueryHandlingComponent(cfg -> new CourseQueryHandler())
        .intercepted(cfg -> (message, context, chain) -> {
            if (!securityContext.canQuery()) {
                return MessageStream.failed(new AccessDeniedException("Query not permitted"));
            }
            return chain.proceed(message, context);
        })
        .build();

Event interceptors

Use EventHandlingComponentsConfigurer.CompletePhase.intercepted() to register an interceptor for all event handling components in a processor. This method closes the component registration phase, so no further components may be added after calling it.

import org.axonframework.messaging.eventhandling.configuration.EventProcessorModule;

EventProcessorModule
        .subscribing("card-summary")
        .eventHandlingComponents(components -> components
                .autodetected("card-summary-projection", cfg -> new CardSummaryProjection())
                .intercepted(cfg -> (message, context, chain) -> {
                    log.info("Handling event: {}", message.qualifiedName());
                    return chain.proceed(message, context);
                })
        )
        .notCustomized();

Multiple event interceptors are also accumulated in registration order:

EventProcessorModule
        .pooledStreaming("card-summary")
        .eventHandlingComponents(components -> components
                .autodetected("card-summary-projection", cfg -> new CardSummaryProjection())
                .intercepted(cfg -> new TracingEventInterceptor(cfg.getComponent(Tracer.class)))
                .intercepted(cfg -> new TenantFilterEventInterceptor(tenantId))
        )
        .notCustomized();

@ExceptionHandler

The @ExceptionHandler annotation is a more specific variant of @MessageHandlerInterceptor. The framework invokes @ExceptionHandler annotated methods only for exceptional results of message handling. This makes it suitable for cross-cutting exception handling concerns such as logging, translating technical exceptions to business errors, or suppressing recoverable failures.

You can wire the exception and the full message (for example, CommandMessage or EventMessage) as parameters. The framework matches an exception handler based on which parameters it declares: the exception type must be assignable from the thrown exception, and the message type must be assignable from the message being handled. Omitting a parameter means it is not used for matching; any value is accepted.

You can introduce @ExceptionHandler annotated methods in any message handling component.

Command handler exception handlers:

class OrderCommandHandler {

    // Command handlers omitted

    @ExceptionHandler
    public void handleAll(Exception exception) {
        // Handles all exceptions thrown within this component
    }

    @ExceptionHandler
    public void handleIllegalStateExceptions(IllegalStateException exception) {
        // Handles all IllegalStateExceptions thrown within this component
    }

    @ExceptionHandler(resultType = IllegalStateException.class)
    public void handleIllegalStateExceptions(Exception exception) {
        // Equivalent: handles IllegalStateExceptions using the resultType attribute
    }

    @ExceptionHandler
    public void logFailedCommand(CommandMessage command, Exception exception) {
        // Access the full command message for cross-cutting concerns such as logging
        log.warn("Command {} failed: {}", command.qualifiedName(), exception.getMessage());
    }
}
Exception handling for static creation handlers

The @ExceptionHandler annotated methods require an existing component instance to work. Because of this, exception handlers do not work for static creation command handlers, since no instance exists when they run.

If you need to handle exceptions from a static creation handler, handle them directly within that method.

Projection exception handlers:

class CardSummaryProjection {

    // Event handlers and query handlers omitted

    @ExceptionHandler
    public void handleAll(Exception exception) {
        // Handles all exceptions thrown within this component
    }

    @ExceptionHandler
    public void handleIllegalArgumentExceptions(IllegalArgumentException exception) {
        // Handles all IllegalArgumentExceptions within this component
    }

    @ExceptionHandler(resultType = IllegalArgumentException.class)
    public void handleIllegalArgumentExceptions(Exception exception) {
        // Equivalent: handles IllegalArgumentExceptions using the resultType attribute
    }

    @ExceptionHandler
    public void logFailedEvent(EventMessage event, Exception exception) {
        // Access the full event message for cross-cutting concerns such as logging
        log.warn("Event {} failed: {}", event.qualifiedName(), exception.getMessage());
    }
}