Projections
A Chronicle projection is an observer that declaratively maps events onto a read model and keeps it up to date.
Your events are the source of truth, but events are a poor thing to read — nobody wants to replay a
thousand MoneyDeposited facts to show an account balance. A projection does that folding for you:
it watches a stream of events and continuously builds a read model — a shaped, queryable view that’s
ready to serve to a UI.
The win is specialization. The same events can feed many projections, each tailored to one screen or question: a balance, a transaction list, a monthly summary. You never reuse one bloated model across conflicting needs — you build a focused read model per view, and each stays simple, fast, and independent.
How you’ll define one
Section titled “How you’ll define one”Start with model-bound attributes, move to fluent IProjectionFor<T> when the mapping needs structure,
and use a reducer when the logic reads better as state transitions; a self-reducing model is the default
for new reducer code. Choose how to build a read model compares the
forms on the same event history and explains when separation or dependency injection warrants an
IReducerFor<T>, when to nominate a plain class, and when tooling-authored PDL fits.
Projections join events (never other read models). Fluent mappings use AutoMap by default;
model-bound projections opt into event mappings with attributes such as [FromEvent<T>].
Choose your consistency
Section titled “Choose your consistency”The one decision worth making up front is when the read model has to be correct:
| Materialized (eventual) | Passive (on demand) | |
|---|---|---|
| When it updates | Shortly after the event is appended (the default) | Computed on demand, without a persistent sink |
| Cost | Cheap reads, scales freely | Every read folds the history from the start |
| Use it for | Almost everything — lists, dashboards, history | A value you must read back correctly right now, such as state a command decides on |
Start materialized. Make a read model passive only when a workflow genuinely can’t tolerate it being a moment behind. When one caller occasionally needs its own write back, it can instead wait for the append’s observers. No projection updates synchronously inside the append, and a read cannot enforce uniqueness — use a constraint for that.
Topics
Section titled “Topics”| Topic | Description |
|---|---|
| Architecture | How the projection engine turns events into read models |
| Choose how to build a read model | One decision path, worked examples, capabilities, and switching costs |
| Model-Bound Projections | Build read models with attributes — the default style |
| Declarative Projections | Build read models with the fluent IProjectionFor<T> API |
| Immediate Projections | Why there is no synchronous projection mode, and what to use instead |
| Eventual Consistency | The default — how and when eventual projections catch up |
| Registration lifecycle | What happens when projections register, change, fail, or are no longer declared |
| Tagging Projections | Organize and tag projections |
| Appended event metadata filters | How tags and metadata correlate reducers and reactors that run alongside projections |
Once your read model exists, expose it to the frontend with a query — or see the whole loop in Build a full-stack feature.