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 updatesJava 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
Related Concepts
| Pattern | Relationship |
|---|---|
| Event-Sourcing-Pattern | Upstream source — event store provides the event stream projections consume |
| CQRS-Pattern | Projections are the CQRS read side; separation of write model from read model |
| Aggregator | EIP structural parallel — both correlate events by ID to build incremental state |
| Domain-Events | Primitive consumed by projections — projections react to domain events to update read model |
Sources
- kurrent.io, Live Projections for Read Models — https://www.kurrent.io/blog/live-projections-for-read-models-with-event-sourcing-and-cqrs
- Axon Framework reference guide — https://www.axoniq.io/products/axon-framework
- Martin Fowler, Event Sourcing — https://martinfowler.com/eaaDev/EventSourcing.html