CQRS and Event Sourcing with Axon Framework: Production Implementation Guide

CQRS and Event Sourcing with Axon Framework

CQRS event sourcing is an architectural pattern that separates read and write operations while storing state changes as an immutable sequence of events. When combined with Axon Framework, Java teams get a battle-tested implementation that handles command routing, event storage, saga orchestration, and read model projections out of the box.

This guide provides a complete production implementation covering domain modeling, event store configuration, read model projections, and deployment strategies. By the end, you will have a working understanding of how to build event-sourced applications that scale independently on the read and write sides.

Understanding the Architecture

In a traditional CRUD application, a single model handles both reads and writes. This creates contention as read optimization (denormalization, caching) conflicts with write optimization (normalization, consistency). CQRS solves this by splitting into separate models.

CQRS event sourcing architecture diagram
CQRS separates command processing from query handling with events as the bridge

Event sourcing complements CQRS by storing every state change as an event rather than overwriting current state. Moreover, this gives you a complete audit trail, time-travel debugging, and the ability to rebuild read models from the event stream.

CQRS + Event Sourcing Flow

Command Side:                    Query Side:
┌──────────┐                     ┌──────────────┐
│ Command  │──▶ Aggregate        │ Read Model   │
│ Handler  │   (validates)       │ (optimized)  │
└──────────┘       │             └──────────────┘
                   ▼                    ▲
              ┌─────────┐              │
              │ Event   │──────────────┘
              │ Store   │  (projections update
              └─────────┘   read models)

Setting Up Axon Framework

Axon Framework provides Spring Boot auto-configuration. Add the dependencies and you get command bus, event bus, and event store wired automatically:

<dependencies>
  <dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>4.9.3</version>
  </dependency>
  <dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-server-connector</artifactId>
    <version>4.9.3</version>
  </dependency>
</dependencies>

Domain Modeling with Aggregates

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private OrderStatus status;
    private List<OrderLine> orderLines;
    private BigDecimal totalAmount;

    // Required no-arg constructor for Axon
    protected OrderAggregate() {}

    @CommandHandler
    public OrderAggregate(CreateOrderCommand cmd) {
        // Validate business rules before producing events
        if (cmd.getOrderLines().isEmpty()) {
            throw new IllegalArgumentException("Order must have items");
        }
        BigDecimal total = cmd.getOrderLines().stream()
            .map(line -> line.getPrice().multiply(
                BigDecimal.valueOf(line.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        AggregateLifecycle.apply(new OrderCreatedEvent(
            cmd.getOrderId(), cmd.getCustomerId(),
            cmd.getOrderLines(), total
        ));
    }

    @CommandHandler
    public void handle(ConfirmOrderCommand cmd) {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException(
                "Can only confirm pending orders");
        }
        AggregateLifecycle.apply(
            new OrderConfirmedEvent(orderId));
    }

    @CommandHandler
    public void handle(CancelOrderCommand cmd) {
        if (status == OrderStatus.SHIPPED) {
            throw new IllegalStateException(
                "Cannot cancel shipped orders");
        }
        AggregateLifecycle.apply(
            new OrderCancelledEvent(orderId, cmd.getReason()));
    }

    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        this.status = OrderStatus.PENDING;
        this.orderLines = event.getOrderLines();
        this.totalAmount = event.getTotalAmount();
    }

    @EventSourcingHandler
    public void on(OrderConfirmedEvent event) {
        this.status = OrderStatus.CONFIRMED;
    }

    @EventSourcingHandler
    public void on(OrderCancelledEvent event) {
        this.status = OrderStatus.CANCELLED;
    }
}

Read Model Projections

Projections subscribe to events and build optimized read models. Therefore, you can create multiple projections from the same events, each optimized for a specific query pattern:

@Component
@ProcessingGroup("order-summary")
public class OrderSummaryProjection {

    private final OrderSummaryRepository repository;

    @EventHandler
    public void on(OrderCreatedEvent event) {
        OrderSummary summary = new OrderSummary(
            event.getOrderId(),
            event.getCustomerId(),
            event.getTotalAmount(),
            "PENDING",
            Instant.now()
        );
        repository.save(summary);
    }

    @EventHandler
    public void on(OrderConfirmedEvent event) {
        repository.findById(event.getOrderId())
            .ifPresent(summary -> {
                summary.setStatus("CONFIRMED");
                summary.setConfirmedAt(Instant.now());
                repository.save(summary);
            });
    }

    @QueryHandler
    public List<OrderSummary> handle(
            FindOrdersByCustomerQuery query) {
        return repository.findByCustomerId(
            query.getCustomerId());
    }

    // Reset handler for replay
    @ResetHandler
    public void reset() {
        repository.deleteAll();
    }
}
Event sourcing projections and read model optimization
Multiple projections from the same event stream enable optimized query patterns

Saga Orchestration

Sagas coordinate processes that span multiple aggregates. Additionally, Axon sagas handle timeouts, compensating actions, and complex business workflows:

@Saga
public class OrderFulfillmentSaga {

    @Autowired
    private transient CommandGateway commandGateway;

    private String orderId;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void on(OrderConfirmedEvent event) {
        this.orderId = event.getOrderId();
        // Reserve inventory
        commandGateway.send(new ReserveInventoryCommand(
            event.getOrderId(), event.getItems()));
        // Set deadline for inventory reservation
        SagaLifecycle.getDeadlineManager()
            .schedule(Duration.ofMinutes(5),
                "inventory-timeout");
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void on(InventoryReservedEvent event) {
        // Proceed to payment
        commandGateway.send(new ProcessPaymentCommand(
            orderId, event.getTotalAmount()));
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void on(PaymentProcessedEvent event) {
        // Schedule shipping
        commandGateway.send(new ShipOrderCommand(orderId));
        SagaLifecycle.end();
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void on(PaymentFailedEvent event) {
        // Compensate: release inventory
        commandGateway.send(
            new ReleaseInventoryCommand(orderId));
        commandGateway.send(new CancelOrderCommand(
            orderId, "Payment failed"));
        SagaLifecycle.end();
    }

    @DeadlineHandler(deadlineName = "inventory-timeout")
    public void onTimeout() {
        commandGateway.send(new CancelOrderCommand(
            orderId, "Inventory reservation timeout"));
        SagaLifecycle.end();
    }
}

When NOT to Use CQRS Event Sourcing

This pattern adds significant complexity. Simple CRUD applications with straightforward data access patterns do not benefit from the overhead. Furthermore, event sourcing requires careful schema evolution — changing event structures across versions demands upcasting strategies. If your team lacks experience with eventual consistency, the debugging challenges of asynchronous projections can be frustrating. Start with CQRS without event sourcing if you only need read/write separation.

Architecture decision framework for CQRS adoption
Evaluate complexity trade-offs carefully before adopting CQRS with event sourcing

Key Takeaways

  • CQRS event sourcing separates reads from writes and stores every state change as an immutable event
  • Axon Framework provides production-ready command handling, event stores, projections, and saga orchestration
  • Read model projections can be rebuilt from the event stream, enabling new query patterns without data migration
  • Sagas coordinate multi-aggregate processes with compensating actions and deadline management
  • Reserve this pattern for domains with complex business rules, audit requirements, and independent scaling needs

Related Reading

External Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top