Skip to content

Read models

A projection says how state is built from events, and until now that was the only way a read model appeared in a document — as the name on the right of an arrow. A view nothing declarative could build had nowhere to go at all: no way to name it, no way to say what it holds, no way to say what folds into it.

readmodel gives it somewhere.

readmodel <Name>
[description "<text>"]
[file <path>]
<property> <Type>[?]
...

The optional file line names the repository relative file this declaration is realized by, so a document can be navigated back to the code it describes. It is additive - it never stands in for any part of the declaration. See File references.

A read model’s file says where the shape lives, which is a different question from where whatever builds it lives - a reducer rule carries its own file, and so does a projection.

A read model declares what it is — its shape, and nothing else:

readmodel AccountBalance
description "What the account is worth right now"
balance Decimal
movements Int

A read model never declares what composes it. Whatever builds it names it, with the same => a projection uses:

projection Deposits => AccountBalance ← a projection builds it
reducer Balance => AccountBalance ← or a reducer does, never both

So there is one arrow and one direction: to find where a read model’s state comes from, you look for the thing pointing at it. A read model that declared its own sources would be a second, opposite arrow saying the same thing, and the two would eventually disagree.

Exactly one thing may build a read model. Two builders is a compile error — either could have produced the value in front of a reader, and nothing in the document would say which. Note this is about the read model, not the slice: a slice may still declare several projections, each building a different read model.

Some views are not expressible as a projection. “Current state plus this event gives the next state” — a running balance that depends on the previous one, a state machine, a computed view that reads what it already holds. reducer is for exactly those:

reducer Balance => AccountBalance
on AmountDeposited
csharp
```
return context.State is null
? new(context.Event.amount, 1)
: context.State with { balance = context.State.balance + context.Event.amount };
```
on AmountWithdrawn
file Reducers/Withdrawn.cs
  • reducer <Name> => <ReadModel> — reads the same way projection <Name> => <ReadModel> does, on purpose.
  • on <EventType> — one rule per event the reducer folds in. A rule with no body is a complete statement that the reducer observes the event, which is the part a reader needs; the reduction itself is code by definition, since it is what a projection could not say.
  • The reduction lives inline in a fenced block, or in a file — the same choice every other construct that needs exact detail offers.

Prefer a projection where one will do. A reducer is code, and code is the part of a document a reader cannot check at a glance.

A rule’s body compiles against ReducerContext, in scope as context, and answers with the read model as it stands after the event:

MemberWhat it holds
Statethe read model before this event — null for the first event on an instance
Eventthe event being folded in
Keythe identity of the instance being built
Tenantthe tenant the events are being reduced for
Occurredwhen the event occurred
SequenceNumberthe event’s position in its sequence
IsFirstwhether State is null, for readability

State is the only nullable member, and that is the whole shape of a reduction: every fold after the first is given what the previous one returned, and the first is given nothing to build on. A rule that ignores the null case is a rule that only works on an instance that already exists.

  • Projections — the declarative way to build a read model, and the first thing to reach for.
  • Queries — how a read model is read once it exists.
  • Commands — how a command declares the state it decides against.