Menu
Course/Performance & Scalability Patterns/CQRS Read Models for Performance

CQRS Read Models for Performance

Optimize reads independently from writes: denormalized read stores, projection patterns, and keeping read models eventually consistent.

12 min readHigh interview weight

The Performance Problem with Shared Models

In a traditional architecture, the same normalized database schema handles both reads and writes. Normalized schemas are optimized for writes — they eliminate redundancy and enforce integrity through foreign keys and joins. But reads typically need to join many tables, sort large result sets, and aggregate data. These competing requirements create a constant tension: every index you add for reads slows down writes, and every normalization decision that helps writes hurts read performance.

CQRS (Command Query Responsibility Segregation) resolves this tension by completely separating the write model (Commands) from the read model (Queries). The write side uses a normalized, integrity-enforcing schema optimized for mutations. The read side uses denormalized projections — separate data stores shaped exactly for the queries the UI needs to answer.

Read Model Architecture

Loading diagram...
CQRS architecture: writes go to the normalized store, events project into the read model

The read model is a projection — a representation of data derived from the write side. When a command succeeds and state changes, the write side publishes a domain event. A projection handler (also called a projector) subscribes to these events and updates the read model accordingly. The read model can be a separate database, a Redis hash, an Elasticsearch index, or even a materialized view in the same database.

Denormalization: Shaping Data for Reads

The power of CQRS read models comes from denormalization. Instead of making the UI join `orders`, `order_items`, `products`, and `customers` at query time, the projector assembles this data once — at write time — and stores the result as a single document per order. The query then becomes a single key lookup.

typescript
// Write side: normalized (source of truth)
// orders: { id, customer_id, status, created_at }
// order_items: { order_id, product_id, quantity, unit_price }
// customers: { id, name, email }
// products: { id, name, sku }

// Read model: denormalized document per order
interface OrderReadModel {
  orderId: string;
  status: string;
  createdAt: string;
  customer: {
    id: string;
    name: string;
    email: string;
  };
  items: Array<{
    productId: string;
    productName: string;
    sku: string;
    quantity: number;
    unitPrice: number;
    lineTotal: number;
  }>;
  orderTotal: number;
}

// Projector: listens to domain events and builds read model
async function handleOrderPlaced(event: OrderPlacedEvent): Promise<void> {
  const customer = await customerRepo.findById(event.customerId);
  const items = await Promise.all(
    event.items.map(async (item) => {
      const product = await productRepo.findById(item.productId);
      return {
        productId: item.productId,
        productName: product.name,
        sku: product.sku,
        quantity: item.quantity,
        unitPrice: item.unitPrice,
        lineTotal: item.quantity * item.unitPrice,
      };
    })
  );
  const orderDoc: OrderReadModel = {
    orderId: event.orderId,
    status: "placed",
    createdAt: event.occurredAt,
    customer: { id: customer.id, name: customer.name, email: customer.email },
    items,
    orderTotal: items.reduce((sum, i) => sum + i.lineTotal, 0),
  };
  await readStore.upsert("orders", event.orderId, orderDoc);
}

Multiple Read Models from One Write Side

A single write store can project into multiple read models, each optimized for a different query pattern. An e-commerce system might have: an order detail model (by order ID), an orders-by-customer model (sorted by date), a product sales model (aggregated by product), and an Elasticsearch index (for full-text search). Each is kept up to date by subscribing to the same domain events.

Eventual Consistency and Lag

The fundamental trade-off of CQRS read models is eventual consistency. After a command succeeds, there is a propagation delay — typically milliseconds to seconds — before the read model reflects the change. This lag comes from event propagation, projection processing, and write-to-read-store latency.

💡

Handling Read-After-Write

If a user places an order and immediately tries to view it, they might see stale data. Common mitigations: (1) Show optimistic UI state immediately based on the command payload. (2) Pass the command's event sequence number to the read API; the read side waits until it has processed up to that sequence number before returning. (3) For critical operations, read directly from the write model for a short window post-command.

Rebuilding Read Models

One of CQRS's superpowers is that read models are derivable — they can always be rebuilt by replaying events from the beginning. This means you can add a brand-new read model at any time (backfill by replaying history), fix a projection bug by deleting and rebuilding, and evolve the read schema independently of the write schema.

💡

Interview Tip

When discussing CQRS read models in an interview, emphasize three things: (1) reads and writes scale independently — you can add read replicas without touching the write path; (2) read models are not a cache — they are the source of truth for reads, rebuilt from events, not from the write DB; (3) eventual consistency is the trade-off. Anticipate the follow-up: 'How do you handle read-after-write consistency?' and have a concrete answer ready.

AspectTraditional (Shared Model)CQRS (Separate Read Model)
Schema optimizationCompromise between reads and writesEach side optimized independently
Query complexityJoins at query timePre-joined documents, single lookup
ConsistencyStrongEventual (seconds of lag)
Schema evolutionWrite + read must migrate togetherRead model rebuilt independently
ScalabilityScale the whole databaseRead and write scale separately
📝

Knowledge Check

5 questions

Test your understanding of this lesson. Score 70% or higher to complete.

Ask about this lesson

Ask anything about CQRS Read Models for Performance