Patterns for Humans software concepts, explained simply
← All concepts
pattern

CQRS

Reading and changing things are different jobs. Stop forcing one model to do both.

The simple version

Command Query Responsibility Segregation just means: use a different model for writing data than the one you use for reading it. They’re allowed to look nothing alike, live in different places, even update at different speeds.

The analogy

Think of a restaurant kitchen versus the menu board out front.

The kitchen’s internal model of “a dish” is a mess of raw ingredients, prep steps, timing, which cook is doing what — optimized entirely for producing food correctly. The menu board’s model of “a dish” is a name, a price, and a nice photo — optimized entirely for a customer deciding what to order.

Nobody would ever try to use the menu board to actually cook, or hand a customer the kitchen’s prep sheet to read. They’re different shapes because they serve completely different purposes — one for doing the work, one for presenting the result — even though they’re ultimately about “the same dish.”

CQRS is admitting the kitchen and the menu board don’t have to be the same document.

The bridge

Most apps start with one model that both reads and writes — usually because it’s genuinely simpler at small scale, and that’s a fine default. But that single model tends to accumulate strain from two directions at once: writes want strict validation and business rules enforced (few fields, tightly controlled); reads want denormalized, pre-joined, fast-to-render shapes (often many fields stitched together from multiple sources, exactly as the UI needs them). Optimizing one side tends to compromise the other.

CQRS splits it in two:

  • Command side — handles writes. Enforces business rules, validates, and is the source of truth. Model stays narrow and strict.
  • Query side — handles reads. Can be a completely different, denormalized shape — sometimes even a different database entirely — built specifically to answer the questions the UI actually asks, with no joins needed at read time.

The two sides sync via events: a command changes state and emits an event (“OrderShipped”), and something updates the query-side model in response. That sync is typically asynchronous, which means the read side can lag the write side by a little — an explicit trade-off, not an accident.

In code

// COMMAND — narrow, validates, is the source of truth
class PlaceOrderCommand {
  constructor(public customerId: string, public items: OrderItem[]) {}
}

async function handle(cmd: PlaceOrderCommand) {
  if (cmd.items.length === 0) throw new Error("Order needs at least one item");
  const order = Order.create(cmd.customerId, cmd.items);
  await orderWriteRepo.save(order);          // strict, normalized model
  await eventBus.publish("OrderPlaced", order);
}

// QUERY — a completely different, pre-shaped read model
// (built from a projection that listens to OrderPlaced and similar events)
interface OrderSummaryView {
  orderId: string;
  customerName: string;   // joined in ahead of time, not at read time
  itemCount: number;
  totalDisplay: string;   // already formatted, e.g. "$42.00"
  status: string;
}

async function getOrderHistory(customerId: string): Promise<OrderSummaryView[]> {
  // one flat, fast read — no joins, no business logic, just presentation
  return db.query("SELECT * FROM order_summary_view WHERE customerId = ?", [customerId]);
}

Nothing about OrderSummaryView needs to match Order’s internal shape — it exists purely to make one specific screen fast and simple to render.

When you don’t need it

Most CRUD apps don’t need this — one shared model for reads and writes is simpler to build, reason about, and keep consistent, and “simpler” wins until it demonstrably doesn’t. Reach for CQRS when read and write workloads have genuinely pulled in different directions: read-heavy screens straining under complex joins, or a write model so choked with reporting concerns that changing a business rule risks breaking a dashboard three teams depend on.