06. Event sourcing — store facts first, rebuild state later¶
⏱️ Estimated time: 16 min | Level: intermediate
ELI5 callback: The town crier pins one notice on the bulletin board. The board rules decide delivery, and the town directory helps readers find it.
Why event sourcing changes the write path¶
CRUD writes the latest value and forgets the journey. Event sourcing writes the journey first and derives the latest value later. See. That choice changes validation, storage, and debugging immediately. A town crier emits a fact after the command passes checks. That fact becomes a notice with type, time, actor, and version. The database now keeps business facts, not only final fields. Diagram:
+---------+ append +------------------+
| command | -------------> | event stream log |
+---------+ +------------------+
| |
v v
validate rules rebuild state
- PlaceOrder is accepted only after stock and limits are checked.
- The system appends OrderPlaced instead of mutating one giant row.
- PaymentConfirmed and ItemPacked arrive as later facts.
- Current order state is reconstructed from that sequence.
- Auditing becomes natural because history already exists.
- Facts should be immutable after commit.
- Event names should reflect business meaning, not table operations.
- Streams are usually grouped by aggregate, like one order or account.
- Commands ask, but events record what actually happened. Simple, no?
Replay turns history into current state¶
Replay means reading past events and applying them in order. Now watch. A fresh service can rebuild state without importing old snapshots first. The bulletin board is effectively the journal every state machine trusts. Handlers stay deterministic, or replay gives different answers later. Pure functions help because the same input should yield the same state. Time-based side effects must stay outside the replay loop. Diagram:
+---------+ +-------------+ +-----------+
| events | -> | apply one | -> | new state |
+---------+ +-------------+ +-----------+
e1 s0 -> s1 running
e2 s1 -> s2 running
e3 s2 -> s3 final
- Start with empty cart state.
- Apply ItemAdded twice and ItemRemoved once.
- The fold result gives one remaining item.
- Apply DiscountApplied and the total changes predictably.
- Re-run tomorrow and the answer must remain identical.
- Replays are great for debugging weird production states.
- Replays also help bootstrap new projections.
- Keep ordering per stream explicit and test it hard.
- Non-deterministic code breaks confidence quickly. Replay is boring by design, and that is a compliment.
Snapshots speed recovery, not truth¶
Long streams are truthful, but they can be slow to rebuild. Snapshots store a checkpoint after many events. See. The snapshot is a cache, not the source of truth. You still keep later events and replay from the checkpoint onward. Choose snapshot frequency using rebuild time, not superstition. Too many snapshots waste storage and create write noise. Diagram:
+----------+ +-----------+ +-----------+
| snapshot | -> | event 91 | -> | state 100 |
+----------+ +-----------+ +-----------+
checkpoint replay current
state remaining result
s90 e91-e100 s100
- Take a snapshot after every 100 order events.
- Restart a worker and load snapshot version 500.
- Replay events 501 through 537 only.
- Reach current state much faster.
- Keep snapshot version aligned with stream version.
- Snapshots reduce startup time.
- Snapshots do not replace the event log.
- Corrupt snapshots can be discarded and rebuilt.
- Measure replay cost before adding snapshot complexity. Fast recovery is nice, but truth still lives in the log.
Projections give many read shapes from one history¶
One event stream can feed many read shapes. Sales wants totals, support wants timelines, and finance wants ledgers. So what to do? Build projections that transform events into query-friendly tables. A clean town directory tells callers which projection answers which query. Projections can lag slightly, so users need honest expectations. This is why read models often look nothing like write models. Diagram:
+--------------+ fan out +----------------+
| event stream | ---------------> | sales view |
+--------------+ +----------------+
| +----------------+
+------------------------> | support view |
+----------------+
- OrderPlaced updates a daily revenue view.
- AddressChanged updates the customer profile view.
- ItemPacked updates the warehouse screen.
- Support timeline appends every meaningful event.
- A failed projection can replay from the stored offset.
- Keep projections disposable and rebuildable.
- Store projection checkpoints separately.
- Handle duplicate events without double-counting.
- Communicate staleness clearly to product teams. Writes care about truth; reads care about shape.
Versioning, idempotency, and retention are the sharp edges¶
The hard part is not saving events. The hard part is evolving them without breaking readers. Now watch. Schema versioning, idempotency, and retention are part of the board rules. Old consumers must ignore new fields safely. New consumers may need adapters for old event versions. Retention policy should respect legal, cost, and replay needs. Diagram:
+-----------+ read old/new +--------------+
| v1 event | -----------------> | upgrader |
+-----------+ +--------------+
| v2 event | -----------------> | same model |
+-----------+ +--------------+
duplicates -> dedup key -> safe side effects
- Add currency_code without deleting old amount semantics.
- Keep a version number in event metadata.
- Upgrade readers before removing deprecated fields.
- Store dedup keys when projections cause side effects.
- Archive cold streams only after replay needs are understood.
- Version forward when possible.
- Never rewrite committed business events casually.
- Test replay on mixed-schema streams.
- Document retention and restore procedures. Event sourcing rewards discipline and punishes shortcuts.
Where this lives in the wild¶
- Stripe-style ledgers, where balances derive from posted entries.
- GitHub audit pipelines, where action history matters beyond current state.
- EventStoreDB setups built around append-only business streams.
- Kafka-backed commerce platforms tracking orders, refunds, and shipments.
- Banking cores that need reconstructable account timelines.
Pause and recall¶
- Why is an event stream more than a debug log?
- When does a snapshot help, and when does it add useless complexity?
- Why must replay code stay deterministic?
- What breaks when schema evolution is handled carelessly?
Interview Q&A¶
Q: Why is event sourcing not the same as audit logging? A: Because events drive state reconstruction and workflows, while audit logs usually describe actions after the main write path. Common wrong answer to avoid: "It is just a bigger debug log with nicer names."
Q: Why are snapshots not the source of truth? A: Because snapshots are derived checkpoints that can be discarded and rebuilt from the committed event stream. Common wrong answer to avoid: "Once a snapshot exists, the old events stop mattering."
Q: How do projections differ from aggregates? A: Aggregates protect write invariants, while projections reshape history into read-friendly views. Common wrong answer to avoid: "Both are just copies of the same table with different indexes."
Q: What is the biggest operational risk in event sourcing? A: Undisciplined event evolution, because one careless change can break replay, projections, and long-term compatibility. Common wrong answer to avoid: "Storage size is the only real issue; everything else stays easy."
Apply now (5 min)¶
Pick one aggregate from your system, like order or wallet. Write five business events in strict sequence. Then write the fold function that rebuilds current state. Add one snapshot point after event four. List one projection for support and one for finance. Note one schema change and how you would version it. Now watch how much design clarity the event list reveals.
Bridge. Event sourcing stores history. But reads need different shapes than writes. → 07