Event Publishing

The publishing of an event is the starting point of such a message. Events have to be published, just like any type of message, before they can be handled. Event publication can originate from a couple of locations within your Axon Framework application. In general, these can be grouped in two major areas:

  1. Publishing events from an entity, and

  2. Publishing events from regular components

Axon provides five interfaces you can use to publish event to your event handlers:

  1. The EventAppender - This component comes into play when publishing events, from for example an entity.

  2. The EventGateway - This component comes into play when publishing events from anything else than an entity.

  3. The EventSink, EventBus, and EventStore - Axon’s infrastructure layering to support different event publication concerns, as described here. Useful to know they exist, but we recommend usage of the EventAppender and EventGateway at all times.

This page will describe how to structure an event and then get it on a publication mechanism. For more specifics regarding event publication and storage implementations in Axon Framework, read this section.

Event publication only results in event storage if an Event Store is used.

Event Appender: Publishing events from an entity

The Entity or its child entities are typically the starting point of all event messages. The event simply is the notification that a decision has been made; a successful resolution of handling a command. Note that tagging is a hard requirement to ensure the published event as associated with your entity or entities.

To publish an event from an entity, it is required to do this from the lifecycle of the entity instance. This is mandatory as we want the entity identifier to be tied to the event message. It is also of the essence that the events originate in order.

The EventAppender provides a simple means to achieve the above:

import org.axonframework.messaging.commandhandling.annotation.CommandHandler;
import org.axonframework.messaging.eventhandling.gateway.EventAppender;

class GiftCard {

    @CommandHandler
    public static GiftCard create(IssueCardCommand cmd, EventAppender appender) {
        appender.append(new CardIssuedEvent(cmd.cardId(), cmd.amount(), cmd.shopId()));
        return new GiftCard();
    }
    // omitted state, command and event sourcing handlers
}

The EventAppender is automatically injected by Axon Framework when declared as a parameter in a command handler. It is the recommended way to append events from within an entity’s command handling methods, as it automatically:

  1. Uses the current ProcessingContext, ensuring transactionality and correlation data propagation.

  2. Attaches the entity identifier to the event.

  3. Publishes the event to registered event handlers within the entity (necessary for event sourcing).

  4. Publishes the event to the EventSink.

The EventAppender can append multiple events at once
@CommandHandler
public static GiftCard create(IssueCardCommand cmd, EventAppender appender) {
    appender.append(
            new CardIssuedEvent(cmd.cardId(), cmd.amount(), cmd.shopId()),
            new CardActivatedEvent(cmd.cardId())
    );
    return new GiftCard();
}

Event Gateway: Publishing events from a non-entity

In the vast majority of cases, entities will publish events by appending them. However, occasionally, it is necessary to publish an event directly to the EventSink, possibly from within another component such as an event handler or a regular service.

The EventGateway provides a convenient API for publishing events from components that are not entities:

import org.axonframework.messaging.eventhandling.gateway.EventGateway;

import java.util.List;
import java.util.concurrent.CompletableFuture;

class Notifier {

    private EventGateway eventGateway;

    public CompletableFuture<Void> publishEvent() {
        return eventGateway.publish(List.of(new CardIssuedEvent("card-1", 100, "Axoniq shop")));
    }
}

The EventGateway automatically wraps your event objects into EventMessage instances before sending them to the EventSink. It provides two main approaches for publishing events:

Publishing without a ProcessingContext

When publishing events from outside a message handler (for example, from an HTTP endpoint or scheduled task), you can use the simple publish method without providing a ProcessingContext. Down below is an example of a Spring REST endpoint doing just that:

import org.axonframework.messaging.eventhandling.gateway.EventGateway;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.CompletableFuture;

@RestController
class CardController {

    private final EventGateway eventGateway;

    public CardController(EventGateway eventGateway) {
        this.eventGateway = eventGateway;
    }

    @PostMapping("/cards/{cardId}/issue")
    public CompletableFuture<Void> issueCard(@PathVariable String cardId,
                                             @RequestBody IssueCardRequest request) {
        return eventGateway.publish(List.of(
                new CardIssuedEvent(cardId, request.amount(), request.shopId()))
        );
    }
}

The EventGateway#publish(List<?) method returns a CompletableFuture<Void>, allowing you to handle the result asynchronously.

Publishing with a ProcessingContext

When publishing events from within a message handler (such as an event handler or query handler), you should provide the ProcessingContext to maintain correlation data:

import org.axonframework.messaging.core.unitofwork.ProcessingContext;
import org.axonframework.messaging.eventhandling.annotation.EventHandler;
import org.axonframework.messaging.eventhandling.gateway.EventGateway;

@EventHandler
public void on(PaymentReceivedEvent event,
               ProcessingContext context,
               EventGateway eventGateway) {
    // Validation logic...

    eventGateway.publish(
        context,
        new BalanceUpdatedEvent(event.getAccountId(), event.getAmount())
    );
}

Providing the ProcessingContext ensures that:

  • Correlation data (trace ID, correlation ID, causation ID) flows from one message to another.

  • The event publication is part of the same processing lifecycle.

  • Distributed tracing works correctly across message boundaries.

Async-native API

All event publishing methods in Axon return a CompletableFuture<Void>. This allows you to:

  • Chain additional operations after publication.

  • Handle errors gracefully.

  • Integrate with reactive frameworks.

public CompletableFuture<Void> publishEvent() {
    return eventGateway.publish(List.of(new CardIssuedEvent("card-1", 100, "Axoniq shop")))
                       .thenRun(() -> logger.info("Event published successfully"))
                       .exceptionally(ex -> {
                           logger.error("Failed to publish event", ex);
                           return null;
                       });
}

Event sink, bus, and store: Low-level event publication

For advanced use cases, Axon Framework provides lower-level components for event publication: the EventSink, EventBus, and EventStore. These components give you more control over the event publication process, but require more manual handling.

For detailed information about these infrastructure components, see the Infrastructure page.

For most use cases, prefer using EventGateway or EventAppender over these lower-level components. The higher-level APIs handle message wrapping and context management automatically, reducing the chance of errors.

Using EventSink directly

The EventSink is the foundational interface for publishing events, which Axon uses to provide a multitude of event sink options. It operates on EventMessage instances rather than plain event objects, giving you full control over metadata:

import org.axonframework.messaging.core.MessageType;
import org.axonframework.messaging.eventhandling.EventMessage;
import org.axonframework.messaging.eventhandling.EventSink;
import org.axonframework.messaging.eventhandling.GenericEventMessage;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

class DirectEventPublisher {

    private final EventSink eventSink;

    public CompletableFuture<Void> publishWithMetadata() {
        EventMessage eventMessage = new GenericEventMessage(
                new MessageType(CardIssuedEvent.class),
                new CardIssuedEvent("cardId", 100, "shopId")
        ).withMetadata(Map.of("userId", "user123", "source", "api"));
        return eventSink.publish(null, eventMessage);
    }
}

The example above shows that when using the EventSink directly, you need to create an EventMessage instance containing your event payload yourself. However, this does give control over the entire structure of your EventMessage, including setting any metadata.

Event Appender and Gateway understand EventMessages

Although the EventSink enforce the EventMessage, you are still able to provide an EventMessage to an EventAppender and EventGateway. Thus, Axon ensures your EventMesage is not wrapped in another EventMessage, as that would be wasteful.

Using EventBus directly

The EventBus extends EventSink and adds the capability to publish events to subscribed event handlers. As with the EventSink, this interface exists to allow Axon to construct other event bus implementations (in the future). We can restructure our example from here and use an EventBus instead:

import org.axonframework.messaging.core.MessageType;
import org.axonframework.messaging.eventhandling.EventBus;
import org.axonframework.messaging.eventhandling.EventMessage;
import org.axonframework.messaging.eventhandling.GenericEventMessage;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

class DirectEventPublisher {

    private final EventBus eventBus;

    public CompletableFuture<Void> publishWithMetadata() {
        EventMessage eventMessage = new GenericEventMessage(
                new MessageType(CardIssuedEvent.class),
                new CardIssuedEvent("cardId", 100, "shopId")
        );
        return eventBus.publish(null, eventMessage);
    }
}

Using EventStore directly

The EventStore combines the functionality of an EventBus with durable storage. For more details on configuring and using an event store, be sure to read this section. We can restructure our example from here and use an EventStore instead:

import org.axonframework.eventsourcing.eventstore.EventStore;
import org.axonframework.messaging.core.MessageType;
import org.axonframework.messaging.eventhandling.EventMessage;
import org.axonframework.messaging.eventhandling.GenericEventMessage;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

class DirectEventPublisher {

    private final EventStore eventStore;

    public CompletableFuture<Void> publishWithMetadata() {
        EventMessage eventMessage = new GenericEventMessage(
                new MessageType(CardIssuedEvent.class),
                new CardIssuedEvent("cardId", 100, "shopId")
        );
        return eventStore.publish(null, eventMessage);
    }
}

Event tagging

When events are published as a result of command handling for an entity, the applicable events should be tagged. These tags are essential to protect your consistency boundary, which is mandatory to protect your entities and aggregates against concurrent writes.

When events are stored, it’s the tags that determine which logical groupings they belong to. This affects:

  • Event sourcing: Tags define which events are loaded when reconstructing entity state. An entity can source events from multiple tag-based streams, not just a single aggregate stream.

  • Consistency boundaries: Tags establish the boundary for write conflict detection. Events with the same tags are checked for concurrent modifications.

  • Event streaming: Event processors can filter events based on tags when streaming from the event store.

Without tags, events would lack the metadata needed to organize them into meaningful boundaries, limiting the flexibility of your event-sourced models.

Configuring tags with @EventTag

The simplest way to tag events is using the @EventTag annotation on event fields:

import org.axonframework.eventsourcing.annotation.EventTag;

record OrderPlacedEvent(
    @EventTag String orderId,
    @EventTag(key = "region") String orderRegion,
    int amount
) {
}

The @EventTag annotation marks fields whose values should be extracted as tags when the event is published. By default, the tag key matches the field name, but you can override it by specifying the key attribute (as shown with "region").

When an OrderPlacedEvent is appended, the TagResolver automatically extracts orderId and region as tags from the annotated fields.

Custom tag resolution

For more complex scenarios where annotation-based tagging is insufficient, you can implement a custom TagResolver:

import jakarta.annotation.Nonnull;
import org.axonframework.eventsourcing.eventstore.TagResolver;
import org.axonframework.messaging.eventhandling.EventMessage;
import org.axonframework.messaging.eventstreaming.Tag;

import java.util.Set;

public class CustomTagResolver implements TagResolver {

    @Override
    public Set<Tag> resolve(@Nonnull EventMessage event) {
        Object payload = event.payload();
        if (payload instanceof OrderPlacedEvent orderEvent) {
            return Set.of(
                    Tag.of("orderId", orderEvent.orderId()),
                    Tag.of("customer", orderEvent.customerId())
            );
        }
        return Set.of();
    }
}

Register your custom TagResolver using the configuration API:

public ApplicationConfigurer configureTagResolver(ApplicationConfigurer configurer) {
    return configurer.eventsourcingConfigurer(esConfigurer ->
        esConfigurer.registerTagResolver(config -> new CustomTagResolver())
    );
}

When using Spring Boot, you can register a custom TagResolver as a Spring bean:

@Configuration
public class AxonConfig {

    @Bean
    public TagResolver tagResolver() {
        return new CustomTagResolver();
    }
}

For detailed information about tags, event criteria, and consistency markers, see Event Store Internals.