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