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.
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();
}
}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.
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
- Saga Pattern Microservices Implementation
- Vertical Slice Architecture Guide
- Data Mesh Architecture Implementation