← Week 3: Event Sourcing & CQRS

Day 16: CQRS — Command Query Responsibility Segregation

Phase 4 · Aug 6, 2026

← Week 3: Event Sourcing & CQRS

Agenda (2–3 hours)

  • Read (45 min): Greg Young "CQRS Documents" §4–6; Martin Fowler "CQRS" pattern article
  • Study (45 min): Design a CQRS system for an order management service: what commands exist? What read models? What events connect them?
  • Practice (45 min): Split a simple CRUD service into a write model (command handlers → events) and two read models (order list projection, order detail projection)
  • Challenge (30 min): When does CQRS add value vs when is it overengineering? Name three cases where CRUD is strictly better
← Week 3: Event Sourcing & CQRS

The Core Idea

CQRS separates the write model (handles commands, emits events) from the read model (optimized for queries):

User → [Command Handler] → Events → [Event Store]
                                           ↓
                                    [Projector]
                                           ↓
                                    [Read DB] ← User queries

Write model is optimized for consistency; read models are optimized for each specific query pattern.

← Week 3: Event Sourcing & CQRS

Commands vs Events vs Queries

Command: intent to change state — may be rejected

PlaceOrder(customer_id, items, payment_info)
CancelOrder(order_id, reason)

Event: something that happened — cannot be rejected (it already occurred)

OrderPlaced(order_id, customer_id, items, total, at)
OrderCancelled(order_id, reason, at)

Query: read without side effects — always succeeds

GetOrderById(order_id) → Order
GetOrdersForCustomer(customer_id) → [Order]
← Week 3: Event Sourcing & CQRS

Projections (Read Models)

A projection is a derived view built by consuming events:

// Order list read model (optimized for listing)
struct OrderListItem {
    order_id: Uuid,
    customer_name: String,
    total: Decimal,
    status: String,
    placed_at: DateTime<Utc>,
}

fn handle_order_placed(event: &OrderPlaced, db: &mut ReadDb) {
    db.insert_order_list_item(OrderListItem {
        order_id: event.order_id,
        customer_name: lookup_customer_name(event.customer_id),
        total: event.total,
        status: "pending".to_string(),
        placed_at: event.at,
    });
}

Projections are rebuilt by replaying events — making new projections from historical data is trivial.

← Week 3: Event Sourcing & CQRS

Eventual Consistency in CQRS

There is a lag between a command completing and the read model being updated:

POST /orders → OrderPlaced event emitted [t=0]
Projector processes event [t=+5ms]
GET /orders → returns updated list [t=+5ms or later]

This is eventually consistent. For strong consistency, read from the write model directly for immediate confirmations — read models are for dashboard/list queries where slight staleness is acceptable.

← Week 3: Event Sourcing & CQRS

Key Takeaways

  • CQRS separates write model (commands → events) from read models (projections)
  • Multiple read models can coexist for different query patterns — each optimized independently
  • Projections are rebuilt by replaying events; adding a new projection is non-destructive
  • Read models are eventually consistent with the write model — acceptable for most display use cases

Tomorrow: Event store design — persistence and optimistic concurrency.