Most systems store the current state of a thing and overwrite it on every change. An event-sourced system stores the changes themselves — an append-only log of facts — and derives current state by folding over that log. Nothing is ever overwritten; you only ever append.
The core equation of event sourcing is a left fold:
state = events.fold(initial, apply)In mnesis that fold is AggregateState: initial() is the seed, and apply
takes the current state plus one event and returns the next state.
impl AggregateState for AccountState {
type Event = AccountEvent;
fn initial() -> Self { Self::default() }
fn apply(mut self, event: &AccountEvent) -> Self {
match event {
AccountEvent::Opened(e) => { self.owner = e.owner.clone(); self.is_open = true; }
AccountEvent::Deposited(e) => { self.balance += e.amount; }
AccountEvent::Withdrawn(e) => { self.balance -= e.amount; }
AccountEvent::Closed(_) => { self.is_open = false; }
}
self
}
}apply is infallible by design. An event is a fact that has already
happened — you cannot reject history. All validation happens before an event
exists, when you decide whether to produce it (see Handle &
Decide).
- A full audit trail, for free. The log is the history. You never ask "how did the balance get here?" — you replay and watch it happen.
- Time travel. Fold up to any point and you have the state as of then.
- New read models retroactively. A projection you invent tomorrow can be built from events recorded years ago — you already have the raw facts.
- The compiler checks your domain. Events are a plain Rust enum; every
applyand every command handlermatches it exhaustively, so a new event variant is a compile error everywhere it must be handled — not a runtime surprise.
Event sourcing splits every state change into two steps mnesis keeps strictly separate:
- Decide — a pure function of
(state, command)that validates and produces new events, or returns a domain error. It changes nothing. - Apply — the infallible fold that folds a produced (or replayed) event into state.
Deciding can fail ("insufficient funds"); applying cannot. Keeping them apart is
what makes rehydration trivial: to reload an aggregate you replay its events
through apply only — no command, no validation, no side effects.
Every event in a stream has a Version (a NonZeroU64, starting at 1, strictly
sequential). The version is how mnesis detects a conflict: an append says "I
expect this stream to be at version N," and the store rejects it if someone else
already wrote N. That is optimistic concurrency — no locks, just a version
check at commit time.
- Domain-Driven Design — how aggregates draw the consistency boundary around a stream.
- CQRS — separating the write model from read models.
- Quickstart — the whole decide → persist → fold → replay loop in runnable code.
- Closing the Books — keeping streams short so the fold stays fast.