The read side
The write side persists events; the read side turns them into something you can query. The component that does the turning is a projection — a function the framework calls once per event, in the order the events were written.
A projection is a fold with a side table
A projection consumes the event stream and folds it into a read model — whatever structure
answers your queries best. In the sample apps that is a couple of SQLite tables (one row per current
entity, plus a history table); it could equally be a search index, a cache, or a document store. The
projection's whole job is small and mechanical: when a CreatedOrUpdated event arrives, upsert the
row; when an Approved event arrives, flip a status column.
The read model's shape is chosen entirely for the questions it answers and owes nothing to the events or to the aggregate's state. That freedom — to denormalize, to keep several differently-shaped views of the same facts — is the entire point of separating reads from writes.
The offset, and exactly-once
A projection remembers how far it has read — an offset into the event stream — and the disciplined move is to write that offset in the same transaction as the rows it produces. Because the data write and the offset bump commit together, the projection is exactly-once even across a crash: on restart it resumes from precisely where it left off and never double-applies or skips an event. When you write your own projection, copy this habit above all others.
Queries never touch the journal
Reads run against the read model, not the event store. A query is just a query — SELECT … FROM
Documents — fast, independent of the write path, and free to use whatever indexing or storage suits
it. The journal is for rebuilding the read model, not for serving it.
Why a read model is disposable
This is the promise from CQRS and event sourcing made concrete. Everything in the read model was put there by the projection, folding events the journal still holds. So if the projection has a bug, or you want an entirely new table, you fix the projection, delete the read model, reset the offset to zero, and let the stream replay from the beginning. The corrected projection rebuilds every row from events that never changed. Edit, drop, replay — no production data surgery, no migration against mismodelled state. The journal is permanent; the read model is a regenerable opinion about it.
The bridge to the client
A projection in FCQRS also returns the events it handled, and those are republished onto a subscription stream. That return value is easy to overlook, but it is what lets a caller learn that the read side has caught up with a specific command — the mechanism behind read-your-writes, covered next in consistency and recovery.
The runnable version is in the tutorial; the recipe is in add a projection.
FCQRS