java

Building Resilient Systems with CQRS and Event Sourcing in Spring Boot

Learn how to implement CQRS and Event Sourcing using Spring Boot and Axon Framework to create scalable, auditable applications.

Building Resilient Systems with CQRS and Event Sourcing in Spring Boot

I’ve been thinking about how we build software lately. Specifically, I’ve been considering the systems that need to be not just functional, but also resilient, auditable, and capable of evolving without breaking. This led me down a path to two powerful patterns: CQRS and Event Sourcing. When combined with the right tools, they can change how you think about application state and data flow. Let’s explore how to bring these concepts to life using Spring Boot and the Axon Framework.

Why does this matter? Think about a typical application. The same data model is often used for both writing data (commands) and reading it (queries). This can create friction. What if you need to optimize a report without affecting the checkout process? What if you need a complete history of every change ever made? Traditional approaches can struggle here.

CQRS, or Command Query Responsibility Segregation, offers a different way. It suggests separating the model for writing data from the model for reading it. They become two distinct sides of the same system. This isn’t just about having two databases; it’s about acknowledging that the needs of a command (like “Place Order”) are fundamentally different from a query (like “Show me last month’s sales”).

But where do these commands come from, and where do they go? This is where Event Sourcing enters the picture. Instead of storing only the current state of an order, you store every single event that happened to it: OrderCreated, ItemAdded, OrderConfirmed, OrderShipped. The current state is just the sum of all these events. Have you ever wished you could rewind your application to see exactly what went wrong? With Event Sourcing, you can replay the events.

So, how do we build this? We’ll use Spring Boot for its simplicity and the Axon Framework because it provides the structure for these patterns. Axon handles the routing of commands, the storage of events, and the updating of query models. Let’s build a simple order management system to see it in action.

First, we set up our project. You’ll need Spring Boot and the Axon dependencies. Here’s a snippet for your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>4.9.1</version>
</dependency>

With the setup ready, we define our domain. Everything starts with a command. A command is an instruction to change the system. It’s a request, not a fact. For our order system, a basic command could be to create an order.

public class CreateOrderCommand {
    private String orderId;
    private String customerId;
    // ... other fields
}

Notice the orderId field. In Axon, this is called the aggregate identifier. It tells the framework which “order” aggregate should handle this command. An aggregate is a cluster of associated objects treated as a single unit for data changes. It’s the guardian of business rules.

When a command is received, an aggregate processes it and, if valid, produces an event. An event is a fact. It’s something that has already happened in the system. It’s immutable. For our CreateOrderCommand, the resulting event would be OrderCreatedEvent.

public class OrderCreatedEvent {
    private String orderId;
    private String customerId;
    private Instant creationTime;
}

This event is then stored in the event store—a journal of every event. The aggregate also updates its own internal state based on this event. How does it know how to update? Through an event handler method, often named on.

@Aggregate
public class OrderAggregate {
    @AggregateIdentifier
    private String orderId;
    private String customerId;
    private OrderStatus status;

    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        this.customerId = event.getCustomerId();
        this.status = OrderStatus.CREATED;
    }
}

The @EventSourcingHandler annotation tells Axon: “Use this method to rebuild the state of this aggregate when loading it from past events.” This is the core of Event Sourcing. The aggregate’s state is not loaded from a table; it’s rebuilt by replaying all its events.

But what about the read side? The user needs to see their orders. This is handled by projections. Projections listen to events and update a separate, optimized database for queries. When the OrderCreatedEvent is published, a projection catches it.

@Component
public class OrderProjection {
    private final JdbcTemplate jdbcTemplate;

    @EventHandler
    public void on(OrderCreatedEvent event) {
        String sql = "INSERT INTO order_view (order_id, customer_id, status) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, event.getOrderId(), event.getustomerId(), "CREATED");
    }
}

Now we have a separation. The write side (the aggregate) deals with commands and events. The read side (the projection) listens to events and updates a simple table for fast queries. They are decoupled. You can change the query table structure without touching the command logic.

What happens when you have thousands of events for a single aggregate? Replaying them all every time is slow. This is where snapshots help. A snapshot is a saved state of the aggregate at a specific point in time. Axon can automatically create them. When loading an aggregate, it loads the latest snapshot and then only replays events that happened after it.

You might wonder, doesn’t this introduce delay? If the query model is updated after the event, what if a user queries immediately? They might see stale data. This is eventual consistency. The write and read models are not instantly synchronized. For many applications, a delay of a few milliseconds is acceptable. It’s a trade-off for scalability and flexibility.

Let’s see a command in action through a REST controller.

@RestController
@RequestMapping("/orders")
public class OrderCommandController {
    private final CommandGateway commandGateway;

    @PostMapping
    public CompletableFuture<String> createOrder(@RequestBody CreateOrderRequest request) {
        CreateOrderCommand command = new CreateOrderCommand(
            UUID.randomUUID().toString(),
            request.getCustomerId(),
            request.getItems()
        );
        return commandGateway.send(command);
    }
}

The CommandGateway is Axon’s entry point for dispatching commands. It sends the command to the appropriate aggregate. The return type is a CompletableFuture, allowing for asynchronous processing.

Building this way requires a shift in thinking. You model the changes (events), not just the state. You design two models instead of one. The benefits, however, are significant. You gain a perfect audit log. You can create new query models from old events without changing the core system. Debugging becomes a matter of replaying history.

It’s not a silver bullet. The complexity is higher. Eventual consistency must be managed. But for systems where the history of data is as important as its current value, this approach is powerful.

I encourage you to start small. Model a single aggregate, like an Order. Define its commands and events. Build a simple projection. See how it feels to have the entire history of an object at your fingertips. The way you view your application’s data might change for good.

What problem in your current work could benefit from having a complete, replayable history of every change? Share your thoughts in the comments below. If you found this walkthrough helpful, please like and share it with other developers who might be curious about these architectural patterns.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: cqrs,event sourcing,spring boot,axon framework,software architecture



Similar Posts
Blog Image
Java 21 Virtual Threads and Structured Concurrency: Complete Developer Guide with Spring Boot Integration

Master Java 21 virtual threads and structured concurrency with practical Spring Boot examples. Learn performance optimization, error handling, and scalability best practices.

Blog Image
Apache Kafka Spring Boot Integration: Building Scalable Event-Driven Microservices Architecture

Learn to integrate Apache Kafka with Spring Boot for scalable event-driven microservices. Build robust real-time systems with asynchronous messaging today.

Blog Image
Complete Guide to OpenTelemetry Distributed Tracing in Spring Boot Microservices

Learn to implement OpenTelemetry distributed tracing in Spring Boot microservices. Step-by-step guide covering setup, Jaeger integration & best practices.

Blog Image
Building Event-Driven Microservices: Apache Kafka and Spring Cloud Stream Integration Guide for Enterprise Applications

Learn to integrate Apache Kafka with Spring Cloud Stream for scalable event-driven microservices. Build robust messaging architectures with simplified configuration.

Blog Image
Master Apache Kafka Spring Boot Integration: Build High-Performance Reactive Event Streaming Applications

Master Apache Kafka and Spring Boot reactive event streaming with practical examples, advanced configurations, schema evolution, and production monitoring techniques.

Blog Image
Apache Kafka Spring Security Integration: Building Secure Event-Driven Authentication for Enterprise Microservices

Learn to integrate Apache Kafka with Spring Security for secure event-driven authentication. Build scalable microservices with real-time security streaming.