Cross-cutting concerns

Behaviour such as tenant scoping, the acting user, audit trails, distributed tracing, and authorisation applies across many commands, queries, events, and sagas — but it is not domain logic. Embedding it in every handler signature couples the model to infrastructure concerns. Spring DDD keeps it separate with two complementary mechanisms that compose cleanly without touching handler or aggregate code.

The two mechanisms

Message metadata

A MessageMetadata envelope of key-value pairs that travels with each dispatched message and with the domain events it produces. Providers attach context on the request thread; handlers declare parameters annotated with @MetadataValue to receive it. MessageMetadata is framework-owned and distinct from Spring Messaging’s MessageHeaders.

Message interceptors

Beans that wrap handler dispatch through an explicit chain, placing cross-cutting behaviour in one location rather than every handler. Interceptors are a Spring DDD bus/dispatch feature — not Spring AOP — and run outside the handler transaction.

The following diagram shows how both mechanisms relate to a single handler invocation:

Metadata providers populate the envelope; interceptors wrap handler dispatch

Which mechanism to use

Use message metadata when a handler — or a downstream event handler — needs to read contextual data (tenant, actor, correlation id). Use message interceptors when you need behaviour around dispatch: audit logging, authorisation vetoes, or timing measurements. They compose naturally: an interceptor commonly reads metadata via CurrentMessageMetadata.get() to make a decision without requiring any change to the handler it wraps.

Routing

A related but distinct cross-cutting concern is where a message goes. Routing can send a command, query, or domain event to an external system — chosen by message type, without changing any call site — while everything with no route continues to dispatch to the local bus. Like metadata and interceptors, its core is message-type-agnostic and lives in the messaging layer; one decision layer covers commands, queries, and events, differing only in how the chosen destination is used.

  • Message metadata — attaching and reading per-message context with @MessageMetadataProvider and @MetadataValue.

  • Message interceptors — wrapping dispatch with @MessageHandlerInterceptor and InterceptorChain.

  • Routing — sending a command, query, or domain event to an external system by message type instead of (or, for events, alongside) a local handler.

  • Clock and time — how the framework resolves its Clock and how to make time deterministic.

  • Serialization customization is documented per-store today; for saga state serialization see Writing a saga.