Skip to main content
Post categories: Code

Event-Driven Design: From Event Definition to Implementation with AxonIQ

26 February 2026 - 4 minutes reading

We are back with a new article on Event-Driven Design (EDD).

In our blog, we have already discussed why combining CQRS and Event Sourcing represents a highly effective architectural choice for complex and scalable systems.

But how can this paradigm be implemented? How do you design and build an application that uses events as its pillars? This article will guide you through the modeling process, starting from the definition of the domain language up to the implementation using Axon Framework.

The starting point: domain language and events

Before you start writing code, it is essential to identify the “language” of the domain. Like true Dutchmen, the creators of Axon often use the modeling of bike rental software as an example.

In this example, the concepts of “Bike reserved” and “Bike rental started” are directly translatable into events within an event-driven architecture.

Below is a snippet of the ReservedBikeEvent class written in Java.

public class ReservedBikeEvent {
    private final String bikeId;
    private final String customerId;
    private final LocalDateTime reservationDate;
}

Modeling Behavior: Commands and Aggregates

Events are the consequence of actions, which in Axon are represented by commands. A Command is a request to perform an action that can change the state.

In EDD, the interaction between commands and events can be modeled as shown in the picture below.

Event-Driven Design: Dalla definizione degli eventi all'implementazione con AxonIQ - EDD model

In Axon Framework, the Aggregate receives and processes commands, forming a fundamental unit in DDD.

The aggregate is a domain object that encapsulates state and business rules, and decides which events to generate in response to a command.

An example: The bicycle booking flow

Below are the steps for booking a bicycle, featuring the Command and Aggregate involved:

  1. An external component sends a Command (for example, ReserveBikeCommand).
  2. The Command is routed to the corresponding Aggregate (for example, ReservationAggregate, which I will show you in the next section).
  3. The Aggregate validates the Command according to its business logic, for instance, by checking if the bicycle is available.
  4. If the Command is valid, the Aggregate generates a new event (for example, ReservedBikeEvent).
  5. The event is permanently saved in the Event Store and published to notify other components in the system.

Aggregate Example: the ReservationAggregate class

Below is the code for the ReservationAggregate class, in Java:

public class ReservationAggregate {
    @AggregateIdentifier
    private String reservationId;
    private BikeStatus status;
    private String bikeId;

    @CommandHandler
    public ReservationAggregate(ReserveBikeCommand command) {
        // 1. Validare il comando.
        if (command.getBikeId() == null) {
            throw new IllegalArgumentException();
        }

        // 2. Generare l'evento.
        apply(new ReservedBikeEvent(
                command.getBikeId(),
                command.getCustomerId(),
                LocalDateTime.now()
        ));
    }

    @EventSourcingHandler
    public void on(ReservedBikeEvent event) {
        // 3. Aggiornare lo stato interno dell'aggregato in base all'evento.
        this.reservationId = UUID.randomUUID().toString();
        this.bikeId = event.getBiciclettaId();
        this.status = BikeStatus.RESERVED;
    }
}

Query Handler: Separating the Read Side

So far, we have modeled the write side. The CQRS pattern requires us to separate the read side, and with Axon Framework, we can manage it using Query Handlers.

When an event is published, one or more Query Handlers can listen to it to update an optimized view of the data, intended exclusively for queries.

Query Handler Example: the RentalStatusProjection class

Let’s continue with the bicycle rental example, featuring the code that implements the RentalStatusProjection Query Handler.

@Component
public class RentalStatusProjection {

    private final BikeStatusViewRepository repository;

    @EventHandler
    public void on(ReservedBikeEvent event) {
        BikeStatusView view = new BikeStatusView(
                event.getBikeId(),
                BikeStatus.RESERVED,
                event.getCustomerId(),
                event.getReservationDate()
        );
        repository.save(view);
    }

    @QueryHandler
    public BikeStatusView handle(QueryBikeStatus query) {
        return repository.findById(query.getBikeId()).orElse(null);
    }
}

This separation allows for scaling and optimizing writing and reading independently.

Orchestrating Complex Processes: Saga

In real-world, complex systems, a single operation can involve multiple aggregates and generate a sequence of events. To coordinate multiple events, Axon provides the Saga pattern (or Process Manager).

A Saga is a component that reacts to events and can emit new Commands depending on the logic with which it was implemented.

How does a Saga work? Here is an example.

  1. The Saga is triggered by an initial event (e.g., ReservedBikeEvent).
  2. Based on its internal state, the Saga can send a command to another aggregate (e.g., UpdateAvailabilityCommand).
  3. It waits for the response event (UpdateAvailabilityEvent) to decide the next step.
  4. It repeats this process until the process is completed.

Advantages of Axon Framework

Axon Framework provides the foundation for applying these concepts, significantly reducing the effort required to reimplement the fundamentals of this paradigm. Among the benefits offered by this AxonIQ solution, I would like to mention:

  • Predefined Infrastructure: It automatically manages the Command Bus, the Event Bus, routing, Aggregate persistence, and transaction management for events.
  • Command Bus: It enables asynchronous and high-performance Command processing, parallelizing tasks wherever possible.
  • Axon Server: It offers a server-side Event Store, message routing, and the AxonIQ Console for monitoring, further simplifying the application of the paradigm.

Conclusions

Modeling an application with AxonIQ means adopting a shift in perspective: from the current state to the history of events that generated it. The journey begins with defining the domain’s event language and takes shape through the implementation of commands, events, and aggregates, as well as projections and sagas to manage complex reads and processes.

This architecture, while involving a modest learning curve, pays off in scalability, traceability, and resilience.

Axon Framework provides the tools to implement this paradigm in real-world projects.

Article written by