Separating the command (write) model from the query (read) model, why CQRS pairs naturally with event sourcing without requiring it, async projections, and when it's overkill vs when it earns its complexity.
Published September 23, 2026
Command Query Responsibility Segregation: the write model and the read model are separate, rather than one unified model serving both.
// Command side — optimized for correctly validating and applying writes
class CreateOrderCommand { String customerId; List<OrderLine> lines; }
class OrderCommandHandler {
void handle(CreateOrderCommand cmd) {
Order order = new Order(cmd.customerId, cmd.lines);
order.validate(); // business rules enforced here
orderWriteRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order)); // triggers read-model update, see below
}
}
// Query side — optimized purely for fast, flexible reads, no business-rule enforcement
class OrderSummaryView { String orderId; String customerName; double total; String status; } // a DENORMALIZED read shape
class OrderQueryService {
List<OrderSummaryView> getOrdersForCustomer(String customerId) {
return orderReadRepository.findSummariesByCustomer(customerId); // reads from a shape built FOR this query
}
}
The write model (Order, enforcing invariants) and the read model (OrderSummaryView, a denormalized shape built specifically for a fast, common query) are different classes, often backed by different storage entirely — the write side optimizes for correctness and enforcing business rules; the read side optimizes purely for query performance, unconstrained by the write model's structure.
CQRS's read model needs to be kept in sync with the write model somehow. Event sourcing (storing every state change as an immutable event, rather than just the current state) is a natural fit — the read model can be built by replaying/projecting those events. But CQRS doesn't require event sourcing: the command side above uses a conventional "save current state" write model, and simply publishes an event on each write specifically to drive the read-model projection — CQRS is about the read/write separation, event sourcing is a specific technique for how state changes are recorded, and they're independently adoptable (commonly paired, not inherently coupled).
@KafkaListener(topics = "order-events")
void onOrderCreated(OrderCreatedEvent event) {
OrderSummaryView view = new OrderSummaryView(event.getOrderId(), event.getCustomerName(), event.getTotal(), "CREATED");
orderReadRepository.save(view); // denormalized, query-optimized shape — updated asynchronously, NOT in the same transaction as the write
}
The read model updates asynchronously, after the write commits and its event is published/consumed — this means the read model is, by construction, eventually consistent with the write model (see Eventual Consistency Design), not immediately consistent. A query issued microseconds after a command completes might not yet reflect it — a real, deliberate tradeoff CQRS accepts in exchange for the read model being freely shaped and scaled independently from write-model constraints.
Most CRUD services genuinely don't need this — a straightforward service where reads and writes both fit comfortably against the same model, with no divergent scaling needs or reporting complexity, gains nothing from the added architectural complexity (two models, an event pipeline keeping them in sync, eventual-consistency handling) and pays real cost in development and operational overhead for it. Reaching for CQRS by default, without a specific problem it's solving, is a textbook over-engineering mistake worth naming explicitly.
Divergent read/write scaling needs: a system with vastly more reads than writes (or vice versa) benefits from scaling the read side independently — a read-optimized, denormalized store that can be replicated/cached aggressively without any write-model constraints slowing it down. Complex reporting requirements: when queries need shapes drastically different from the natural write model (a dashboard aggregating data across many entities in ways the write model was never designed to support efficiently), a purpose-built read model avoids contorting the write model to serve reporting needs it wasn't designed for.
Q: Does CQRS mean the read and write models must use different databases? A: Not necessarily — CQRS can be implemented with both models in the same database (different tables/views), or genuinely different databases entirely (a relational write store, a search-optimized or denormalized read store) — the database topology is an implementation choice; the defining characteristic of CQRS is the model separation itself, not where each model physically lives.
Q: How would you handle a query that needs data that's still 'in flight' (the write committed but the read-model projection hasn't caught up yet)? A: This is exactly the read-your-own-writes problem from Eventual Consistency Design — options include falling back to the write model directly for that specific just-written entity, or accepting the brief staleness and designing the UI to communicate a 'processing' state, the same tradeoff discussed there.
Q: What's a concrete risk of the read-model projection logic having a bug? A: The read model silently diverges from the write model's actual state — since queries never touch the write model directly, a projection bug can persist for a long time before being noticed (unlike a write-model bug, which tends to surface faster since it directly affects business operations), which is why testing projection logic thoroughly and monitoring for read/write model drift matters more in a CQRS system than it would in a simpler, unified-model service.
Q: Is CQRS at odds with the Data Ownership Model's database-per-service principle? A: No — CQRS's read/write split typically happens WITHIN a single service's own data ownership boundary (that service owns both its write model and its own derived read model), not across service boundaries — it's a refinement of how one service internally manages its own data, not a mechanism for services to share or duplicate ownership of the same data.