Skip to content

Projections and read models

A read model is a view of your data shaped for reading: one screen, one report or one API response. A projection is the rule that builds it. It watches events as they are appended and keeps the read model up to date.

In an event-sourced system the events are the truth, but they are a poor thing to query. Showing a balance by replaying every deposit and withdrawal on each request would be slow and wasteful. Projections do that folding once, as events arrive, and store the result so reads are cheap. This page covers both terms together because they are two halves of the same job. It follows from what is event sourcing? and is the read side of CQRS.

A projection subscribes to one or more event types. For each event it receives, it changes the read model for the matching entity. The read model is stored in a database, and queries read from there.

updates

query

BookRegistered

Projection

BookBorrowed

BookReturned

Read model: BookStatus

Screen or API

Four properties define the model:

  • Specialised. Build one read model per question. The same events can feed a balance, a transaction list and a monthly summary, each with its own shape.
  • Derived. You never write to a read model directly. If it is wrong, fix the projection and replay the events.
  • Rebuildable. Because the events are kept, you can create a new read model later by replaying the history into it.
  • Eventually consistent. A read model is usually updated shortly after the event is appended, not inside the same operation.

Projections join events, not other read models. A projection reads from the event log and writes to its own read model.

The terms overlap, so here is how Chronicle uses them.

TermWhat it is
Read modelThe shaped, queryable result
ProjectionA declarative rule that maps events onto a read model
ReducerCode that folds each event into the current state, one event at a time

Chronicle offers three ways to define how events build a read model:

StyleWhat it looks likeUse it when
Model-bound projectionAttributes on the read model typeThe default; most mappings fit
Declarative projectionA fluent IProjectionFor<T> classThe mapping needs more explicit control
ReducerAn IReducerFor<T> classState transitions are easier to write as code

Chronicle’s choose a read-model style page builds the same read model all three ways.

A library tracks whether each book is on the shelf or on loan. Three events carry the story:

using Cratis.Chronicle.Events;
[EventType]
public record BookRegistered(string Title, string Isbn);
[EventType]
public record BookBorrowed(string MemberName);
[EventType]
public record BookReturned;

View C# snippet source on GitHub

The screen needs a small read model: the title, whether the book is on loan and who has it. In a model-bound projection, the read model describes how events fill it:

using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
public record BookStatus(
[Key] string Id,
[SetFrom<BookRegistered>] string Title,
[SetFrom<BookRegistered>] string Isbn,
[SetValue<BookBorrowed>(true)]
[SetValue<BookReturned>(false)]
bool IsBorrowed,
[SetFrom<BookBorrowed>(nameof(BookBorrowed.MemberName))]
[ClearWith<BookReturned>]
string? BorrowedBy);

View C# snippet source on GitHub

Chronicle finds the projection from the attributes. There is no registration code and no separate class. Title and Isbn map from the event property of the same name. IsBorrowed is set to a constant by each event. BorrowedBy is set from the borrow event and cleared by [ClearWith<BookReturned>] on the return event.

When the mapping needs more control, the same read model can be defined as a projection class. AutoMap, the default for declarative projections, fills Title and Isbn from the event properties of the same name, so only the other members are mapped explicitly:

using Cratis.Chronicle.Projections;
public class BookStatusProjection : IProjectionFor<BookStatus>
{
public void Define(IProjectionBuilderFor<BookStatus> builder) => builder
.From<BookRegistered>(_ => _
.Set(m => m.Id).ToEventSourceId()
.Set(m => m.IsBorrowed).ToValue(false)
.Clear(m => m.BorrowedBy))
.From<BookBorrowed>(_ => _
.Set(m => m.IsBorrowed).ToValue(true)
.Set(m => m.BorrowedBy).To(e => e.MemberName))
.From<BookReturned>(_ => _
.Set(m => m.IsBorrowed).ToValue(false)
.Clear(m => m.BorrowedBy));
}

View C# snippet source on GitHub

Use one style per read model, not both. Both examples above are excerpts and assume a registered Chronicle client.

When you want to fold state with ordinary code, a reducer receives the event, the current state and the event context, and returns the new state. A running balance is a good fit:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;
[EventType]
public record DepositMade(decimal Amount);
[EventType]
public record WithdrawalMade(decimal Amount);
public record AccountBalance(decimal Balance, DateTimeOffset LastUpdated);
public class AccountBalanceReducer : IReducerFor<AccountBalance>
{
public AccountBalance Deposited(DepositMade @event, AccountBalance? current, EventContext context) =>
new((current?.Balance ?? 0m) + @event.Amount, context.Occurred);
public AccountBalance WithdrawalMade(WithdrawalMade @event, AccountBalance? current, EventContext context) =>
new((current?.Balance ?? 0m) - @event.Amount, context.Occurred);
}

View C# snippet source on GitHub

To read the result, ask for the instance by key:

BookStatus? status = await eventStore.ReadModels.GetInstanceById<BookStatus>(bookId);
if (status is not null)
{
Console.WriteLine($"{status.Title}: {(status.IsBorrowed ? status.BorrowedBy : "on the shelf")}");
}

View C# snippet source on GitHub

In an Arc application, the same read model carries a [ReadModel] attribute and a static query method, and the frontend gets a typed proxy. See read models in Arc with Chronicle.

By default, a projection is materialized: the read model is stored and updated after the append. Reads are cheap and scale well. Occasionally a workflow cannot tolerate a read model that is a moment behind. Chronicle then offers a passive read model, which is not stored but computed from its events each time you read it. It always includes the latest append, at the price of replaying that instance’s history.

Start with materialized. Switch to passive only where you must read your own write at once. Neither option enforces a rule such as uniqueness; use a constraint for that. The details are in read model consistency.

  • Fast reads. The work is done at write time, so queries read a ready-made view.
  • Focused models. Each screen gets exactly the fields it needs, with no joins at read time.
  • Freedom to change. You can add, change or drop a read model without touching the history.
  • Rebuilds instead of migrations. A new view is created by replay.
  • Isolation. A slow or broken projection does not block appends.
  • Eventual consistency. A read right after a write may show old data. Design screens for it, or use a passive read model where needed.
  • More artefacts. Every view is a projection to maintain.
  • Replay cost. Rebuilding a large history takes time.
  • Not for decisions that must see the latest state. Do not enforce rules by reading a read model and then appending. Two concurrent commands can both pass the check. Use a constraint or a concurrency check on the write side.
  • Not needed for trivial reads. A CRUD application with no history does not need projections at all.
  • One giant read model for every screen. It brings back the coupling you set out to avoid. Build specialised models.
  • Treating a read model as the source of truth. It can be dropped and rebuilt. The events are the record.
  • Putting business rules in a projection. A projection mirrors facts. Decisions belong where commands are handled.
  • Forgetting that a replay reruns side effects. A projection only builds a read model, so replaying it is safe. A reactor that sends an email is different; see event-driven architecture vs. event sourcing.
  • Unbounded child collections. A read model that collects every event of a busy entity grows without limit. Keep child lists bounded or split the view.
  • Querying the event log directly for screens. Use a read model.

What is the difference between a projection and a read model?

Section titled “What is the difference between a projection and a read model?”

The read model is the result, and the projection is the rule that produces it. People often say “projection” for both.

Is a read model the same as a materialized view?

Section titled “Is a read model the same as a materialized view?”

They are close. Both store a precomputed result. A read model is built from events by an observer rather than from a SQL query, and it can live in a document or relational store.

Replay the events through the projection. Chronicle can replay an observer from the start of the event log. Because projections only build state, replay does not repeat side effects.

Can a projection use data from another read model?

Section titled “Can a projection use data from another read model?”

No. Projections join events. If a view needs data from two entities, join the events from both.

In a sink, which is MongoDB by default. See Chronicle’s projections documentation for what is configurable.