~ / track F / concurrency models

Event sourcing

Advanced

Event 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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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.

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate
 status: new