๐Ÿ”ฅ 0
โญ 0
Lesson 3 of 6 18 min +200 XP

Building Read Models with Projections

You have an event stream with thousands of OrderPlaced, OrderShipped, and OrderDelivered events. But your dashboard needs to show "orders by region this month" and your API needs to return "order details by ID". How do you serve these different queries efficiently?

Projections transform your event stream into purpose-built read models.

What is a Projection?

A projection is a function that:

  • Subscribes to events from the event stream
  • Transforms each event into updates for a read model
  • Maintains a denormalized, query-optimized view
Event Stream                    Projections                 Read Models
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”‚ OrderPlaced    โ”‚
โ”‚ PaymentReceivedโ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ OrderDetails โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ [Order lookup by ID]
โ”‚ OrderShipped   โ”‚              Projection
โ”‚ OrderDelivered โ”‚
โ”‚ ...            โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ SalesDashboard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ [Revenue by region]
โ”‚                โ”‚              Projection
โ”‚                โ”‚
โ”‚                โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ CustomerHistory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ [Orders by customer]
                                Projection

Multiple Views, Same Events

Order Details Projection

Optimized for: Lookup by order ID

{
  orderId: "ORD-123",
  customerName: "John",
  items: [...],
  status: "shipped",
  tracking: "1Z999"
}

Sales Dashboard Projection

Optimized for: Aggregations by region/time

{
  region: "US-West",
  month: "2024-01",
  orderCount: 1547,
  revenue: 234567.89
}

Customer History Projection

Optimized for: Orders by customer

{
  customerId: "CUST-456",
  totalOrders: 23,
  lifetimeValue: 4567.89,
  recentOrders: [...]
}

Building a Projection

// The projection handler processes events and updates the read model
class OrderDetailsProjection {
  private readDb: ReadDatabase;

  // Handle each event type
  async handle(event: OrderEvent) {
    switch (event.type) {
      case 'OrderPlaced':
        await this.readDb.insert('order_details', {
          orderId: event.orderId,
          customerId: event.customerId,
          customerName: event.customerName,  // Denormalized!
          items: event.items,
          totalAmount: event.totalAmount,
          status: 'pending',
          placedAt: event.timestamp
        });
        break;

      case 'PaymentReceived':
        await this.readDb.update('order_details',
          { orderId: event.orderId },
          { status: 'paid', paidAt: event.timestamp }
        );
        break;

      case 'OrderShipped':
        await this.readDb.update('order_details',
          { orderId: event.orderId },
          {
            status: 'shipped',
            trackingNumber: event.trackingNumber,
            carrier: event.carrier,
            shippedAt: event.timestamp
          }
        );
        break;

      case 'OrderDelivered':
        await this.readDb.update('order_details',
          { orderId: event.orderId },
          { status: 'delivered', deliveredAt: event.timestamp }
        );
        break;
    }
  }
}

Projection Lifecycle

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     PROJECTION LIFECYCLE                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  1. SUBSCRIBE        2. PROCESS           3. STORE                  โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€     โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€       โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€             โ”‚
โ”‚  โ”‚ Event Stream โ”‚โ”€โ”€โ”€โ–ถโ”‚ Projection  โ”‚โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ Read Model โ”‚           โ”‚
โ”‚  โ”‚              โ”‚    โ”‚ Handler     โ”‚      โ”‚ Database   โ”‚           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚
โ”‚                                                                     โ”‚
โ”‚  4. QUERY (Fast!)                                                   โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€              โ”‚
โ”‚  โ”‚ API Request โ”‚โ”€โ”€โ”€โ–ถโ”‚ Read Model โ”‚โ”€โ”€โ”€โ–ถโ”‚ Response โ”‚                 โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                 โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Power of Rebuild

One of the most powerful features of projections: you can rebuild them from scratch.

Scenario: Bug in Sales Dashboard

Your sales dashboard was calculating revenue wrong for 3 months. In a traditional system, you'd need complex data migration scripts. With event sourcing:

  1. Drop the sales_dashboard read model table
  2. Fix the projection code
  3. Replay all events from the beginning
  4. Projection rebuilds with correct data
class ProjectionRebuilder {
  async rebuild(projection: Projection) {
    // 1. Clear existing read model
    await projection.clear();

    // 2. Get all events from the beginning
    const events = await this.eventStore.getAllEvents();

    // 3. Process each event through the projection
    for (const event of events) {
      await projection.handle(event);
    }

    console.log(`Rebuilt projection with ${events.length} events`);
  }
}

Handling Eventual Consistency

Projections are eventually consistent - there's a delay between an event being written and the read model being updated.

THE PROBLEM

  1. User places order (command)
  2. OrderPlaced event saved
  3. User redirected to "My Orders"
  4. Projection hasn't processed yet!
  5. User doesn't see their order

SOLUTIONS

  • Return new data in command response
  • Optimistic UI updates
  • Polling with backoff
  • WebSocket notifications
  • Read-your-writes consistency

Real-World: Event Store Subscriptions

// Using EventStoreDB's subscription API
const subscription = client.subscribeToAll({
  fromPosition: lastProcessedPosition
});

for await (const event of subscription) {
  // Route to appropriate projection(s)
  await orderDetailsProjection.handle(event);
  await salesDashboardProjection.handle(event);
  await customerHistoryProjection.handle(event);

  // Track position for restart recovery
  await saveCheckpoint(event.position);
}

Projection Patterns

Pattern Description Use Case
Single Projection One projection per aggregate type Simple CRUD-like reads
Cross-Aggregate Combines events from multiple aggregates Dashboard, reporting
Search Index Projects to Elasticsearch/Algolia Full-text search
Notification Triggers emails, webhooks, alerts External integrations

Real-World: Netflix Billing

Netflix's Multiple Projections

From the same billing events, Netflix builds multiple projections:

  • Account Projection: Current subscription status for the app
  • Invoice Projection: Detailed billing history for customer service
  • Revenue Projection: Financial reporting for accounting
  • Analytics Projection: Churn prediction, cohort analysis

Key Takeaways

  • Projections build read models - Transform events into query-optimized views
  • Multiple views, same events - Each projection serves different query needs
  • Rebuild anytime - Fix bugs by replaying events through corrected code
  • Eventually consistent - Design your UI to handle the delay gracefully

Next up: Event Store - How to persist and replay your events reliably.

๐Ÿง  Quick Quiz

Test your understanding of this lesson.

1

What is a projection in Event Sourcing?

2

Why can you have multiple projections from the same events?

3

What happens if a projection has a bug and produces wrong data?

Event Sourcing Basics