Products Consulting About Blog Contact Us Česky
arrow_back Back to blog

The Silent Metadata Gap: Why Your Axon 5 Events Have No User Attribution

The Silent Metadata Gap: Why Your Axon 5 Events Have No User Attribution

When we migrated from Axon Framework 4 to 5, we carefully updated aggregates to use EventAppender, rewrote command handlers with static factory methods, and adjusted our event sourcing handlers. Everything compiled. Tests passed. Events flowed. Then during an internal QA round, we spotted something odd: the aggregate history timeline showed events with no author — just timestamps and event types floating in a void.

The root cause? Axon command metadata does not automatically propagate to events. And when you’re focused on the big Axon 5 migration changes, this is exactly the kind of thing that slips through.

The Setup: User Attribution via Command Metadata

Our architecture uses an AuthAwareCommandGateway — a thin wrapper around Axon’s CommandGateway that extracts the authenticated user from Spring Security’s reactive context and attaches userId and userName as command metadata:

Java
@Service
@RequiredArgsConstructor
public class AuthAwareCommandGateway {

    private final CommandGateway commandGateway;
    private final ReactiveAuthenticationSupplier authenticationSupplier;

    public <R> Mono<R> send(Object command, Class<R> resultType) {
        return authenticationSupplier.getAuthentication()
            .map(auth -> {
                Metadata metadata = Metadata.with("userId", auth.getName());
                String userName = extractUserName(auth);
                if (userName != null && !userName.equals(auth.getName())) {
                    metadata = metadata.and("userName", userName);
                }
                return metadata;
            })
            .defaultIfEmpty(Metadata.emptyInstance())
            .flatMap(metadata -> {
                CommandResult result = commandGateway.send(command, metadata);
                return Mono.fromFuture(result.resultAs(resultType));
            });
    }
}

On the read side, our aggregate history service reads events from Axon Server via gRPC and extracts userId/userName from the event metadata to render a timeline:

Text
● Record Updated — Petr — 5 minutes ago
● Record Created — Sofia — 23 hours ago

Except it was rendering:

Text
● Record Updated — 5 minutes ago
● Record Created — 23 hours ago

No user. For any event. For any entity type.

The Root Cause: Two Separate Message Envelopes

The following diagram shows the full message flow from the GraphQL mutation down to the event store — and where the metadata gap occurs:

sequenceDiagram participant UI as Vue.js UI participant GQL as DGS Mutation participant GW as AuthAwareCommandGateway participant Bus as Axon CommandBus participant Agg as Aggregate participant ES as Axon Event Store UI->>GQL: updateRecord(input) GQL->>GW: send(UpdateRecordCommand) Note over GW: Extracts auth from
ReactiveSecurityContext GW->>GW: Attach metadata:
{userId, userName} GW->>Bus: dispatch(command + metadata) Bus->>Agg: handle(UpdateRecordCommand, EventAppender) Note over Agg: eventAppender.append(
new RecordUpdatedEvent(...)) rect rgb(255, 230, 230) Note over Agg,ES: ❌ WITHOUT CorrelationDataProvider
Event metadata = {} (empty!) end Agg->>ES: store event Note over ES: Event stored with
NO user attribution

In Axon Framework, a command and the events it produces are fundamentally different messages with separate metadata envelopes. When your aggregate’s command handler creates events via EventAppender.append():

Java
@CommandHandler
public void handle(UpdateRecordCommand command, EventAppender eventAppender) {
    eventAppender.append(new RecordUpdatedEvent(command.recordId(), command.data()));
}

The RecordUpdatedEvent starts with a fresh, empty metadata map. The userId and userName that were attached to the UpdateRecordCommand stay on the command — they don’t magically transfer to the event.

This is by design. Not all command metadata should be persisted into the event store. Auth tokens, tracing correlation IDs, retry counters — you probably don’t want those stored permanently with every event. Axon makes metadata propagation an explicit, opt-in concern.

The Fix: CorrelationDataProvider

Axon provides the CorrelationDataProvider interface specifically for this purpose. It answers: “When a message is being handled, which metadata keys should be copied from the incoming message to any messages produced during handling?”

The simplest implementation is SimpleCorrelationDataProvider, which copies named metadata keys:

Java
@Configuration
public class AxonCorrelationDataConfig {

    @Bean
    public CorrelationDataProvider userMetadataCorrelationDataProvider() {
        return new SimpleCorrelationDataProvider("userId", "userName");
    }
}

That’s it. One bean. Axon 5’s CorrelationDataProviderAutoConfiguration automatically discovers CorrelationDataProvider beans in the Spring context and registers them with the CorrelationDataProviderRegistry. From that point on, every event produced while handling a command that carries userId or userName metadata will inherit those values.

How It Works Under the Hood

When a command arrives at the aggregate, Axon’s infrastructure wraps the handling in a ProcessingContext. Before the command handler executes, the CorrelationDataInterceptor extracts correlation data from the incoming command using all registered providers. When EventAppender.append() is called, the appended event’s metadata is enriched with the correlation data before it’s committed to the event store.

sequenceDiagram participant GW as AuthAwareCommandGateway participant Bus as Axon CommandBus participant CDP as CorrelationDataInterceptor participant Agg as Aggregate participant EA as EventAppender participant ES as Axon Event Store GW->>Bus: command + metadata:
{userId, userName} Bus->>CDP: intercept before handling Note over CDP: Extract correlation keys
from command metadata CDP->>Agg: handle(UpdateRecordCommand, EventAppender) Agg->>EA: append(RecordUpdatedEvent) rect rgb(230, 255, 230) Note over EA: ✅ Enrich event metadata
with correlation data:
{userId, userName} end EA->>ES: store event with full metadata Note over ES: Event stored WITH
user attribution ✅

Why This Was Hard to Catch

Several factors made this bug particularly stealthy:

  1. No error, no exception. Events are stored perfectly fine with empty metadata. The aggregate works. Projections update. The only symptom is missing user attribution in the UI, which is easy to overlook during development.

  2. Axon 4 had the same requirement. This isn’t new to Axon 5. In Axon 4, you also needed a CorrelationDataProvider — typically configured via Configurer.configureCorrelationDataProviders(). If you never configured one in Axon 4 either, you never had user attribution in events, and the migration wouldn’t change that.

  3. The command gateway “works.” AuthAwareCommandGateway correctly attaches metadata to commands. Logging confirms it. If you debug the command dispatching, everything looks right. The gap is invisible until you inspect the stored events in Axon Server.

  4. Tests mock the event store. Unit tests for command handlers verify that the correct events are emitted, but they don’t test metadata propagation — that’s infrastructure behavior outside the aggregate’s responsibility.

Beyond User Identity: What Else to Propagate

Once you have the pattern established, consider propagating other cross-cutting concerns:

Java
@Bean
public CorrelationDataProvider auditCorrelationDataProvider() {
    return new SimpleCorrelationDataProvider(
        "userId",
        "userName",
        "traceId",       // Distributed tracing correlation
        "sourceSystem"   // Web UI vs API vs scheduled job
    );
}

The traceId is particularly valuable for correlating events back to the original HTTP request in distributed tracing systems like OpenTelemetry. And sourceSystem lets you distinguish whether a change was made through the web UI, a scheduled job, or an API integration — useful for both audit trails and debugging.

Key Takeaways

  1. Command metadata does not propagate to events automatically. This is true in both Axon 4 and 5. Register a CorrelationDataProvider bean for any metadata keys you want on your events.

  2. Silent data loss is the worst kind of bug. No exceptions, no failed tests — just empty fields in your UI. Add assertions in integration tests that verify event metadata contains expected keys.

  3. Axon 5’s Spring Boot autoconfiguration is your friend. Just define CorrelationDataProvider beans and they’ll be picked up automatically — no manual Configurer calls needed.

  4. Historical events won’t be retroactively fixed. Events already stored without user metadata stay that way. Only new events produced after the fix will carry user attribution. If you need retroactive attribution, you’d need to upcaste historical events — or accept the gap.


Cover photo by Conny Schneider on Unsplash

More from the Blog