The Eternal Ledger
Accountants have known for centuries: don't erase, append. A ledger records every transaction---deposits, withdrawals, transfers. You never delete an entry. If a mistake is made, you add a correction. The history is sacred.
This ancient wisdom has profound implications for how we design systems. What if, instead of storing what is, we stored what happened?
Two Ways to Remember
Consider our chess game. We could remember it in two ways:
The Snapshot: Store the current position.
current_state:
board: [positions of all pieces]
turn: black
move_count: 23The Story: Store what happened.
events:
1. pawn e2 → e4
2. pawn e7 → e5
3. knight g1 → f3
...
23. bishop c1 → g5Both contain the same information---from the events, we can reconstruct any snapshot. But they represent fundamentally different philosophies of memory.
Events as Source of Truth
In event sourcing, we make a radical choice: the events are the source of truth. The current state is merely a derivative---something we compute when we need it.
source_of_truth:
events: [event_1, event_2, ..., event_n]
derived (computed when needed):
current_state = apply_all(events, starting from initial_state)Why would we do this? Because events have properties that snapshots lack:
- Immutable
- — An event that happened cannot un-happen. pawn e2 → e4 at move 1 is eternal.
- Ordered
- — Events form a sequence. We know what came before and after.
- Meaningful
- — Events carry intent. "Player moved pawn" is richer than "pawn is now at e4."
The Append-Only Log
An event log is append-only. We only add to the end. We never modify or delete.
log:
[event_1] // after move 1
[event_1, event_2] // after move 2
[event_1, event_2, event_3] // after move 3
...This simplicity has profound consequences:
- No lost history --- Every state the system ever had can be reconstructed
- Audit trail --- We can always answer "what happened and when?"
- Debugging --- When something goes wrong, we can replay to find where
What Makes a Good Event?
Not every change should be an event. Good events are:
Domain-meaningful: They describe something that matters in the problem domain.
// Good: captures intent
PlayerMovedPiece(player: white, piece: knight, from: g1, to: f3)
// Less good: too low-level
SquareCleared(square: g1)
SquareFilled(square: f3, piece: knight)Complete: They contain everything needed to understand what happened.
// Good: self-contained
PlayerMovedPiece(player: white, piece: knight, from: g1, to: f3,
timestamp: 2024-01-15T10:30:00Z)
// Incomplete: what piece? from where?
PieceMoved(to: f3)Past tense: Events describe what did happen, not what should happen.
// Good: fact about the past
PieceWasMoved(...)
// Not an event: a command (request for future action)
MovePiece(...)Commands vs Events
This distinction is crucial:
Command: MovePiece(knight, g1, f3)
// A request. Might be rejected if invalid.
Event: PieceWasMoved(knight, g1, f3)
// A fact. Already happened. Cannot be undone.The flow is:
1. User issues Command
2. System validates Command against current state
3. If valid: system records Event
4. Event is appended to log (forever)
5. State is updated (derived from events)The Eternal Return
In an event-sourced system, nothing is ever truly deleted. The past persists eternally in the log.
// A game from years ago
game_2847_events:
1. pawn e2 → e4 // still here
2. pawn e7 → e5 // still here
... // all of it, foreverThis has philosophical weight. Every decision, every move, every moment---preserved. The system remembers everything.
For some applications, this is exactly right: financial transactions, legal records, medical histories. For others, the right to be forgotten matters. Event sourcing makes forgetting hard by design.
We've stored the story. But how do we read it? In the next chapter, we learn to travel through time---replaying events to reach any moment in history.