All event-sourcing patterns
Production pattern444 words · verified 10 September 2026

Event-Sourced Projections and Read Models

A projection folds ordered events into a query-specific read model. It is derived state, not a second source of truth: operators must be able to discard it, replay source events, and build a replacement without rewriting event history or stopping existing readers.

Problem

Why this pattern exists

Event streams optimize durable history, not every screen or report. Replaying thousands of events per request wastes latency and compute, while embedding every query shape into write model couples new product questions to old command paths. Projections turn one history into several purpose-built views.

Rebuildability changes migration design. Instead of mutating a live table in place, deploy projection v2 beside v1, replay history into it, compare results and lag, then move readers. This works only when reducers are deterministic and external enrichment has already been captured as events or stable metadata.

Design decisions

Make boundaries explicit

  1. 01

    One view per access pattern

    Model fields, indexes, and retention around actual reader. An order summary, operational queue, and finance export can consume same facts while keeping independent schemas and release cycles.

  2. 02

    Make reducer deterministic

    Given same ordered events and initial state, reducer must produce same output. Avoid wall-clock reads, random IDs, and network lookups inside fold; record those inputs as facts first.

  3. 03

    Version projection identity

    Treat reducer or output-schema change as new projection version. Run old and new views together until rebuild completes and verification passes.

AllSource implementation

Apply pattern to durable Core history

AllSource keeps Core as durable event database. Query Service owns tenant-facing projection compute and exposes HTTP, realtime, and analytics reads. For custom Rust read models, ProjectionWorker consumes events through Core's durable-consumer protocol, applies a synchronous reducer, and tracks a server-side WAL cursor.

Name custom workers by projection version, filter event types narrowly, and retain last applied entity version for deduplication. ProjectionWorker checkpoints cursor position, not reduced state, so restart replays events after checkpoint. If cold rebuild is large, persist projection state in its own serving store or use Core entity snapshots as a starting point, while keeping source events authoritative.

Side-by-side projection migration
orders_summary_v1  cursor 8,240,118  serving readers
orders_summary_v2  cursor 7,992,450  replaying history

verify counts + sampled states
wait until v2 cursor reaches live head
switch readers to v2
retire v1 after rollback window

Failure modes

Detect weak implementations early

Projection cannot rebuild without calling mutable external APIs.

Fix: Capture external result as an event before projection consumes it.

Reducer change silently alters existing live view.

Fix: Create versioned projection and replay beside old reader path.

Per-tenant computed state leaks across boundaries.

Fix: Enforce tenant scope before fold and keep tenant-facing projections in Query Service.

Production checklist

Ready when each statement is true

  • Every projection has named reader and access pattern.
  • Reducer is deterministic, total, and idempotent.
  • Projection version changes with reducer or schema.
  • Rebuild progress, lag, errors, and last checkpoint are observable.
  • Cutover and rollback work without changing source events.

Related patterns

Store history once

Rebuild every useful view from durable events.