CQRS (Command Query Responsibility Segregation)
One model for both reading and writing data works fine until reads and writes want completely different shapes, or completely different scale. This module is what happens when you finally split them.
Learning objectives
- Beginner: Explain the difference between a command and a query in CQRS terms.
- Intermediate: Describe why a write model and a read model might use entirely different data stores.
- Advanced: Explain how CQRS's read/write split interacts with eventual consistency between the two sides.
◆ The problem
A normalized relational schema (Databases Mastery — SQL) is exactly right for WRITES — it prevents duplicate or inconsistent data. But a dashboard needing to display an aggregated, denormalized view across five joined tables, on every single page load, fights against that same normalized structure — the shape that's correct for writing is often actively wrong for the read pattern you actually need.
CQRS's answer: stop using ONE model for both. Commands (writes) go through a model optimized for correctness; Queries (reads) go through a SEPARATE model optimized for exactly the shape the read side needs. (NoSQL Foundations' own CQRS chapter covers this pattern generically, independent of any specific framework — this module picks up from there and shows the concrete Spring/Kafka wiring that actually keeps the two models in sync.)
In a simple CQRS implementation, this can just mean two different sets of classes reading/writing the SAME database. In a more advanced implementation, the read model lives in an ENTIRELY different data store — a denormalized document in MongoDB or a search-optimized index in Elasticsearch — kept in sync with the write side's relational database via events, using the exact publish/consume mechanism from the Kafka & Microservices category.
// Command side public void placeOrder(PlaceOrderCommand cmd) { Order order = new Order(cmd); orderRepository.save(order); // normalized, write-optimized eventPublisher.publish(new OrderPlacedEvent(order)); } // Read side (separate service/module, reacting to the event) @KafkaListener(topics = "order-events") public void onOrderPlaced(OrderPlacedEvent event) { orderSummaryReadModel.upsert(event.toSummaryView()); // denormalized, read-optimized }
💻 Code example
public void placeOrder(PlaceOrderCommand cmd) { Order order = new Order(cmd); orderRepository.save(order); eventPublisher.publish(new OrderPlacedEvent(order)); } @KafkaListener(topics = "order-events") public void onOrderPlaced(OrderPlacedEvent event) { orderSummaryReadModel.upsert(event.toSummaryView()); }
⚠ Common real-world trap
The moment a command commits, the WRITE side is immediately consistent — but the READ side only updates once the event has propagated and been processed, which is a real, non-zero delay. A user placing an order and immediately refreshing a dashboard reading from the CQRS read model can genuinely see stale data for a brief window — this needs to be a deliberate, communicated trade-off, not a surprise bug discovered in production.
✓ Quick recap
- CQRS splits writes (commands) and reads (queries) into separate models, instead of forcing one schema to serve both well.
- The read model can live in a completely different, denormalized data store, kept in sync via events.
- This trades strict consistency for read-side flexibility and performance — the read model is eventually consistent with the write model, not instantly.
Want a visual for this concept?
Generate a diagram tailored to “CQRS (Command Query Responsibility Segregation)” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →