CQRS — Command/Query Responsibility Segregation — is the idea that the model you use to change state and the model you use to read state do not have to be the same. Writes go through aggregates that decide events; reads come from purpose-built projections folded from those events.
Event sourcing and CQRS are separable ideas that pair extremely well: the event log is the single source of truth for the write side, and any number of read models can be derived from it.
Write side (command). A command loads an aggregate, decides events, and appends them. It is optimised for enforcing invariants, one aggregate at a time. This is Model a Domain.
Read side (query). A projection folds the event log into a shape optimised for reading — a table, a summary, a search index. It enforces no invariants; it just answers questions fast.
The two sides never share a model. A change to how you read never risks a domain invariant, and a change to how you decide never breaks a query.
mnesis ships the read side as four composable primitives, and deliberately no event loop:
Projector— the fold:initial()+apply(state, &event) -> Result. Unlike an aggregate'sapply, a projector's is fallible — a read model may do checked arithmetic and legitimately reject bad input.Subscription— the cursor that feeds the fold events in order (see Subscriptions).PersistTrigger— when to persist progress (EveryNEvents, or after specific event types).SnapshotStore— atomic(state, position)persistence, so a restart resumes instead of re-folding from zero (see Snapshots).
The Projection stepper assembles these into an inert per-event stepper —
load → advance(state, event) → flush — but it owns no loop. You drive
it from whatever runtime you already have (a tokio while let, an actor
mailbox). Shipping a loop would make mnesis a runtime; it stays a kernel.
// The consumer owns the loop; mnesis owns the fold, trigger, and checkpoint.
let (stepper, mut state) = Projection::load(/* projector, trigger, snapshot store, id, schema */).await?;
while let Some(event) = subscription.next().await {
state = stepper.advance(state, event?).await?;
}
stepper.flush(&state).await?;See the runnable projection-tokio
example
for a complete loop.
Lifecycle, supervision, back-pressure, passivation, and cursor management are runtime concerns that differ wildly between a server and an IoT device. Baking one policy into mnesis would duplicate — and fight — whatever runtime you deploy into. So mnesis hands you the pure pieces and stays out of the control flow.
- Aggregates — the write side.
- Subscriptions — the event feed a projection reads.
- Snapshots — checkpointing projection progress.
- Event Sourcing — the log both sides build on.