Projections

Projections

"A projection is a function that takes an event stream and produces a read model." — paraphrased from kurrent.io/blog/live-projections-for-read-models-with-event-sourcing-and-cqrs

Intent

A Projection is a pure function (or stateful event handler) that consumes an event stream and produces a denormalised read model optimised for query access. Since the read model is derived entirely from the event log, it can be discarded and rebuilt at any time — this rebuild capability is the core value proposition that distinguishes CQRS/ES read models from traditional query caches.

Projections ARE the CQRS read side when used within a CQRS architecture: the command side emits events to the event store; projections consume those events and maintain read-optimised models. However, projections can also be used independently with Event Sourcing for materialised views without a full CQRS command bus.

When NOT to Use

  • Systems without an event store or event log — projections require an append-only event source; without it, there is nothing to subscribe to or replay from
  • Simple queries that can be served directly by the write model — if read/write shapes are identical and volume is symmetric, projections add unnecessary indirection
  • When strong consistency is required on every read — projections are eventually consistent by nature; a query immediately after a command may return stale data until the projection catches up

When to Use

  • Read model needs a different shape than the write model — denormalised for fast queries (single document per aggregate, no joins)
  • Multiple read models are needed from the same event stream — different views for different consumers (admin dashboard vs. mobile API vs. analytics pipeline)
  • Read model corruption must be recoverable by replay — no manual data surgery; rebuild is deterministic and repeatable
  • Audit or historical views are required — replay to any point in time produces a consistent historical snapshot

How It Works

Projections operate in two modes:

Incremental (live): The projection subscribes to the event stream and updates the read model in real time as new events arrive. The read model is kept warm; individual event processing is fast.

Full rebuild: Truncate the read model → subscribe to the event store from position 0 (catchup subscription) → replay all historical events through the projection handler. After replay completes, the projection switches to incremental mode for new events.

The rebuild path is what distinguishes projections from traditional caches. The event log is the single source of truth. Any projection can be regenerated deterministically — a bug in projection logic is fixed by correcting the handler and rebuilding from scratch. No manual SQL migration required.

Projection handler contract:

  • Pure handler: given an event, update read model state
  • Idempotent: replaying the same event twice should not corrupt the read model
  • No side effects beyond read model mutation during rebuild phase

Flow Diagram

flowchart LR
    subgraph Event Store
        S1[order-ord-1 stream]
        S2[order-ord-2 stream]
    end

    subgraph Projection Handler
        PH[OrderProjection<br/>handler function]
    end

    subgraph Read Model
        RM[(Denormalised DB<br/>or Cache)]
    end

    S1 -->|subscribe / replay| PH
    S2 -->|subscribe / replay| PH
    PH -->|upsert per event| RM

    Q[Query API] -->|read| RM

    style Event Store fill:#fff3e6,stroke:#d98c00
    style Read Model fill:#e6ffe6,stroke:#4a9d4a

    Note1[Incremental: live subscription<br/>updates in real time]
    Note2[Full rebuild: truncate read model<br/>replay from position 0]

    PH -.-> Note1
    PH -.-> Note2

TypeScript Example

// Projections — TypeScript (read model rebuild from event store)
// Source: kurrent.io/blog/live-projections-for-read-models-with-event-sourcing-and-cqrs
import { EventStoreDBClient } from '@eventstore/db-client';
 
const readModel = new Map<string, { orderId: string; status: string }>();
 
async function rebuildProjection(client: EventStoreDBClient): Promise<void> {
  readModel.clear();                                     // truncate: discard old read model
  const events = client.readStream('$all', { fromPosition: 'start' });
  for await (const { event: e } of events) {
    if (!e) continue;
    // Apply each historical event to rebuild read model from scratch
    if (e.type === 'OrderPlaced') {
      readModel.set(e.data.orderId, { orderId: e.data.orderId, status: 'placed' });
    }
    if (e.type === 'OrderConfirmed') {
      readModel.get(e.data.orderId)!.status = 'confirmed';
    }
  }
}
// Query: readModel.get(orderId) — fully derived from event log, rebuildable on demand
// Production: use persistent subscriptions (not readStream) for live incremental updates

Java Example

// Projections — Java (Axon Framework @EventHandler on projection component)
// Source: axoniq.io reference guide; Baeldung CQRS + Event Sourcing guide
@Component
public class OrderProjection {
    private final Map<String, OrderView> readModel = new ConcurrentHashMap<>();
 
    @EventHandler
    public void on(OrderCreatedEvent event) {
        readModel.put(event.getOrderId(),
            new OrderView(event.getOrderId(), "CREATED"));
    }
 
    @EventHandler
    public void on(OrderConfirmedEvent event) {
        readModel.computeIfPresent(event.getOrderId(),
            (id, view) -> view.withStatus("CONFIRMED"));
    }
 
    @QueryHandler
    public OrderView handle(GetOrderQuery query) {
        return readModel.get(query.getOrderId());
    }
 
    // @ResetHandler — called by Axon before replaying all events for full rebuild
    // Axon handles rebuild: reset triggers @ResetHandler, then replays all events
    // through @EventHandler methods — same handler, deterministic outcome
}

Lineage Backward

  • Event-Sourcing-Pattern — the event store is the source for projection rebuild; the append-only event log is what makes deterministic rebuild possible
  • CQRS-Pattern — projections are the CQRS read side; the command side emits events that projections consume to maintain denormalised read models
  • Aggregator — projection handler structurally parallels the EIP Aggregator pattern: correlates events by aggregate ID and accumulates incremental state from a stream

Lineage Forward

  • Enterprise-Patterns-MOC — Phase 16 enterprise patterns MOC will include read model and projection as a section in the event-driven architecture cluster
PatternRelationship
Event-Sourcing-PatternUpstream source — event store provides the event stream projections consume
CQRS-PatternProjections are the CQRS read side; separation of write model from read model
AggregatorEIP structural parallel — both correlate events by ID to build incremental state
Domain-EventsPrimitive consumed by projections — projections react to domain events to update read model

Sources