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
- 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.
- 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.
- 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.
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 windowFailure 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
Continue through adjacent decisions
Durable subscriptions
A durable subscription gives a named consumer a server-tracked position in the event log. After reconnect, event store replays committed events after last acknowledged position, then switches consumer to live delivery. Processing remains at least once, so handlers must be idempotent.
Read nextEvent replay
Event replay reads immutable events again in their original stream order and applies them to a new or reset consumer. Use replay to rebuild projections, reproduce historical state, test new reducers, or backfill derived outputs—never to re-trigger uncontrolled external side effects.
Read nextSnapshots and checkpoints
A snapshot stores derived aggregate state at a known point in its history; a checkpoint stores how far a consumer processed the event log. Both speed recovery, but neither replaces source events. Snapshot validity depends on reducer compatibility, while checkpoint validity depends on consumer identity and delivery semantics.
Read nextStore history once
