The first time I implemented CQRS in earnest was because a single data model could no longer keep up: writes needed strict transactional integrity, and reads needed to serve thousands of queries per second with filters that the transactional model wasn't designed to resolve well. Trying to make a single table, a single schema, meet both goals had us fighting the database instead of solving the business problem.
CQRS (Command Query Responsibility Segregation) starts from a simple idea: separate the model that receives commands (writes) from the model that answers queries (reads). What almost nobody tells you when they propose it in a meeting is the real cost of that separation.
The pattern, without box-and-arrow diagrams
The command side validates business rules and persists the change as the source of truth. The query side maintains one or more "projections" — copies of the data, optimized for reading, that get updated asynchronously based on what happened on the command side.
// Command side: validates and persists the intent
async function handleCreateOrder(cmd: CreateOrderCommand) {
const order = Order.create(cmd.customerId, cmd.items) // business rules here
await orderRepository.save(order)
await eventBus.publish(new OrderCreatedEvent(order.id, order.items))
}
// Query side: updates by reacting to the event, not the command
async function onOrderCreated(event: OrderCreatedEvent) {
await orderSummaryReadModel.upsert({
orderId: event.orderId,
itemCount: event.items.length,
status: 'created',
})
}
Nothing exotic. The complexity isn't in the code, it's in what happens between those two functions.
The cost: eventual consistency
Between when the command is saved and the read projection is updated, there's a window of time —milliseconds, almost always—during which the system is in an inconsistent state: the write has already happened, but if you query the read view you won't see it reflected yet. That's eventual consistency, and it's not an implementation flaw: it's a direct consequence of separating the models.
The mistake I see most often is treating that window as if it didn't exist, until a user creates an order, immediately refreshes the page, doesn't see it, and opens a support ticket convinced that the system lost their order. The system didn't lose anything. The architecture simply didn't tell anyone there would be a delay.
When it's worth it (and when it's over-engineering)
CQRS makes sense when the read and write patterns are genuinely different: high read frequency with very varied query shapes, writes that require heavy business validation, or the need to scale reads and writes completely independently.
It doesn't make sense for standard CRUD where reading and writing the same model works perfectly fine. I've seen teams drop full CQRS — with an event bus, separate projections, and different storage for each side — into systems that would have been just as well served by a table and an index. The question I ask myself before proposing it isn't "is this a valid pattern?", it's "does the problem I have justify the operational complexity I'm about to introduce?".
How to live with the inconsistency window
If you decide it's worth it, there are reasonable ways to keep eventual consistency from feeling like a bug:
- Optimistic UI: update the interface with the expected result as soon as the command is accepted, without waiting for the read projection to confirm.
- Version tokens: attach the version of the data the client saw to the command, so the system can decide whether the state that was read is stale before accepting a change.
- Idempotent projections: design the event consumer so that processing the same event twice doesn't break anything — retries and duplicate deliveries are the norm, not the exception, in event-based systems.
- Communicate the delay when it matters: if there's an operation where the user needs to see their change reflected immediately, consider reading directly from the write model for that specific case, instead of forcing everything through the eventual projection.
CQRS doesn't solve the consistency problem, it makes it explicit. And in architecture, making explicit a trade-off that already existed —but was hidden— is almost always worth more than pretending it doesn't exist.