Hello! Welcome back to our series on distributed data management.
In the previous lesson, we established the "what" and "why" of the Command Query Responsibility Segregation (CQRS) pattern. We learned that by separating the models for writing data (commands) and reading data (queries), we can independently optimize and scale each side, which is a massive advantage in complex, high-traffic microservice systems.
Today, we move from theory to practice. Our learning outcome is to implement the command side of a CQRS system to handle state changes and the query side to expose denormalized read models. We'll build a complete, albeit simple, CQRS flow using Spring Boot, demonstrating how the pieces fit together. Mastering this implementation is crucial for justifying architectural choices in a system design interview.
1. A Quick Architectural Refresher
Let's bring back the diagram from our last lesson to anchor our discussion. The system we are about to build will mirror this architecture.

Our implementation will consist of:
- A Command Side: Built with Spring Data JPA and a relational database (like PostgreSQL). It will handle
POSTrequests to create or update data. - A Query Side: Built with Spring Data MongoDB. It will use a denormalized document model tailored for fast reads and will handle
GETrequests. - An Event Bus: Using Apache Kafka, this will be the asynchronous link that propagates changes from the command side to the query side.
2. Implementing the Command Side
The command side's sole purpose is to process commands that change the system's state. Its key responsibilities are:
- Validate the incoming command.
- Persist the state change to the write-optimized data store.
- Publish an event to notify the rest of the system about the change.
To see how this works in practice, let's examine a straightforward implementation.
CQRS Pattern Implementation in Spring Boot with Kafka
The article "CQRS Pattern Implementation in Spring Boot with Kafka" provides a clear and concise example of building the command side. We'll focus on how a state change is persisted and then published as an event.
Please read section '6. Command Side Implementation'. Pay close attention to the components: Entity: Product is a standard JPA entity representing our write model. Service: The ProductCommandService orchestrates the logic. Notice how it first saves the entity to the database and then uses the ProductEventProducer to send the new state to a Kafka topic. Controller: The ProductCommandController is a standard REST controller that exposes an endpoint to trigger the command.
As you can see from the code, the ProductCommandService is the heart of the command side. It executes the business logic, ensures data is saved transactionally in the write database (PostgreSQL in this case), and then reliably publishes an event. This event, ProductEvent, contains the information needed for the query side to update its own model.
3. Implementing the Query Side
The query side is responsible for providing fast and efficient data reads. It achieves this by listening for events from the command side and building "projections"—denormalized read models specifically designed to answer certain queries.
Let's see how the other half of our system is built.
CQRS Pattern Implementation in Spring Boot with Kafka
Continuing with the same article, let's now build the query side that consumes the events and serves the read-optimized data.
Please read section '7. Query Side Implementation'. Focus on: Query Entity: ProductView is a MongoDB document. Notice that it's a different shape than the Product entity; it's a denormalized model containing only the data needed for display. Kafka Consumer: The ProductEventConsumer is our projector. It listens to the product-events topic. When it receives a message, it transforms the data and saves it to the read database (MongoDB). Service & Controller: The ProductQueryService and ProductQueryController are very simple. They just fetch data from the MongoDB repository without any complex logic or joins.
The magic of the query side is its simplicity at read time. All the hard work of assembling the data is done ahead of time by the ProductEventConsumer when the event is processed. When a client requests a list of products, the ProductQueryController can retrieve it with a very fast and simple query to MongoDB.
Test your understanding!
Suppose you need to add functionality to update the price of a product. Using the architecture from the article, describe the flow of an UpdatePrice command.
- What would the REST endpoint in the
ProductCommandControllerlook like? - What would the
ProductCommandServicedo? - What event would be published?
- How would the
ProductEventConsumeron the query side handle this event?
Show answer
- Controller: The
ProductCommandControllerwould have a newPUTorPATCHmapping, perhaps@PutMapping("/{id}"), that accepts a product ID and a request body with the new price. - Service: The
ProductCommandServicewould have a new method likeupdateProductPrice(Long id, Double newPrice). This method would:- Find the
Productentity in the PostgreSQL database. - Update its price field.
- Save the updated
Productentity. - Publish a
ProductUpdatedEvent(or a similar event) to Kafka containing the product's ID and new price.
- Find the
- Event: A
ProductUpdatedEventwould be published to theproduct-eventstopic. - Consumer: The
ProductEventConsumerwould receive this new event. It would then find the correspondingProductViewdocument in MongoDB using the product ID and update itspricefield with the new value from the event.
4. Advanced Implementation: CQRS with Event Sourcing
The previous example used a standard CRUD-style write database. A more advanced and powerful pattern, often discussed in senior-level interviews, is to combine CQRS with Event Sourcing (ES).
In Event Sourcing, we don't store the current state of an entity. Instead, we store an immutable, append-only log of all the events that have ever happened to that entity. The current state is then derived by replaying these events. This event log becomes the ultimate source of truth.
Let's look at a more complex, production-grade example that uses this approach.
Java Spring EventSourcing and CQRS Clean Architecture ...
The article 'Java Spring EventSourcing and CQRS Clean Architecture microservice' provides an excellent, in-depth implementation of this advanced pattern. We won't read the whole article, but we will examine the key components that differentiate this from the simpler CQRS implementation.
Please review the code in these sections to understand the flow: Section 6: BankAccountCommandHandler: Notice that when handling a command (e.g., ChangeEmailCommand), the service first loads the aggregate from the eventStoreDB. This load operation replays events to reconstruct the current state. It then applies the change, which creates a new event, and calls eventStoreDB.save(aggregate) to persist this new event—not the state. Section 7: BankAccountMongoProjection: This is the projector for the query side. It listens to a Kafka topic that streams the events from the event store. The when(Event event) method is a router that updates the MongoDB BankAccountDocument based on the specific type of event it receives (e.g., EmailChangedEvent, BalanceDepositedEvent). Section 8: BankAccountQueryHandler: This component is very similar to our simple example. It fulfills query requests by reading directly from the MongoDB repository, which holds the denormalized read model created by the projection.
The key shift with Event Sourcing is that the write model is no longer a table with the current state but a log of historical facts. This provides a full audit trail and enables powerful capabilities, like re-building read models or debugging issues by replaying events. The query side's role remains the same: to create optimized projections from this stream of events.
Conclusion
In this lesson, we bridged the gap between the theory and practice of CQRS. You have seen two ways to implement the pattern, from a straightforward state-based approach to a more advanced event-sourced architecture.
Key Takeaways:
- Command Side Implementation: Involves a controller to receive commands, a service to orchestrate logic, a write-optimized repository (e.g., JPA), and an event producer to announce state changes.
- Query Side Implementation: Consists of an event consumer (the "projector") that updates a read-optimized repository (e.g., MongoDB), and a simple query service and controller to serve the denormalized data.
- Synchronization: Asynchronous communication via a message broker like Kafka is the standard way to connect the two sides.
- Event Sourcing + CQRS: A powerful combination where the write model is an immutable log of events, providing a rich source of truth for building any number of read model projections.
Next Up
We've now built a system that is, by design, eventually consistent. There's a small but non-zero delay between a command succeeding and the result being visible on the query side. What does this mean for the user experience? How do we handle it?
In our next lesson, we will analyze the trade-offs of eventual consistency and describe strategies to mitigate user-experience issues. This is a critical follow-up that addresses the most significant challenge of using CQRS and other distributed data patterns.
Can't find a good explanation? Sign up and we'll make it for you
Sign up