~ / track F / concurrency models
Event sourcing
AdvancedEvent sourcing stores your system's history as an append-only log of events and derives the current state by folding over them. Where a normal CRUD system overwrites a row, an event-sourced system writes an event ("OrderPlaced", "ItemShipped") and keeps every one forever. Current state becomes a cached view of the events, not the source of truth.
The basic shape
State = reduce apply-event initial-state events.
That's almost the whole idea. In Clojure:
The "balance" is derived. Replay the events on another node and you get the same balance; replay only a subset and you get the past balance.
Why this matters
- Perfect audit trail. Every change to the system is on disk, with enough context to understand why the state moved.
- Time travel. Roll the event log forward to any point. Debugging a customer issue becomes "fold the events up to the moment of the bug."
- Multiple views. The same event stream can feed many read models: current balance, monthly statement, fraud detection, ML training data.
- Functional core, naturally. The reducer (event → state) is pure. Side effects live at the edges: appending events, projecting them into views.
Events are facts, not commands
A key discipline: an event records something that happened, in the past
tense, as data. :money-deposited is an event; :deposit-money is a
command. Commands can be rejected; events cannot. The handler that
processes a command may validate it (against the current state) and then
emit zero or more events.
Command → Validator (current state) → Event(s) → Log → Projections
This separation is what gives event sourcing its CQRS flavor (Command Query Responsibility Segregation): the write side and read sides have different shapes because they have different jobs.
Snapshots: a pragmatic optimization
For long-lived aggregates, replaying every event from the dawn of time gets
slow. The standard answer is snapshots: periodically persist the current
state and a pointer ("up to event N"); on load, start from the snapshot and
replay only the tail. The pure reducer makes this trivial — a snapshot is
just a precomputed state.
Failure modes to watch
- Schema evolution. Events live forever; old events written before a schema change must still be replayable. The reducer needs upgraders.
- Event store size. Every event ever, plus indexes, plus projections. Storage is cheap, but compaction and projection rebuild costs grow.
- Idempotency at the edges. Multiple consumers may process the same event; they must be safe to retry.
Check yourself
? quiz
In event sourcing, why is it important that events are stored in the past tense and treated as facts?
Exercise
Extend the example reducer to support a :transfer-out and :transfer-in
pair of events (modeling a transfer between two accounts as two events). Then
compute the final balances for two accounts given a list of events. Note
how neither event modifies both accounts — the reducer runs per-account.