Event Sourcing
Store immutable events as truth and fold them to derive state, bound replay with snapshots, guard writes with expected-version optimistic concurrency, correct by appending a compensating event (never editing), and use it only where audit/temporal value justifies the complexity.
Store the events, derive the state
Most systems store current state and mutate it in place: a balance column gets UPDATEd and the previous value is gone. Event sourcing inverts this. The source of truth is an append-only, immutable log of events ("Deposited $100", "Withdrew $30"), and current state is derived by replaying (folding) those events over an aggregate. You never overwrite; you only append. The current balance is a computed view, not the stored truth.
Deriving state
An aggregate (say Account #42) has a fold function: start from an empty state, apply each event in order, produce the current state. fold(events) = events.reduce(apply, initialState). Because the log is ordered per aggregate, replay is deterministic: the same events always yield the same state. This is why an event-sourced ledger can answer "what was the balance as of last Tuesday 5pm": you fold only the events up to that timestamp. In-place-state systems cannot answer that without a separate audit table, because they threw the history away.
Why teams reach for it
(1) Full audit trail for free: every change is a first-class, retained fact, which regulators love for financial and medical systems. (2) Temporal / time-travel queries: reconstruct any past state. (3) Debugging: replay production events into a fixed build to reproduce a bug exactly. (4) New read models retroactively: a new projection can be built by replaying the entire history (see the CQRS lesson).
The replay-cost problem, and snapshots
An account open for 10 years may have 100k events. Folding all of them on every read is too slow. The fix is snapshots: periodically persist the derived state (e.g. every 500 events) as Snapshot(version=N, state=...). To load, take the latest snapshot and fold only the tail of events after it. Replay cost becomes bounded by snapshot frequency, not aggregate age. Snapshots are a cache, never the source of truth: you can always delete every snapshot and rebuild from the log.
Concurrency: optimistic, via expected version
Two requests both read Account #42 at version 100 and both try to append. To prevent a lost update, each append carries an expected version. The store's append is conditional: "append this event only if the aggregate is still at version 100." The first wins and moves to 101; the second's condition fails, and it retries by re-reading the now-current state. This is optimistic concurrency and it is the standard event-store contract (EventStoreDB, or a Postgres table with a unique constraint on (aggregate_id, version)).
Interview nuance: you never edit history. A wrong event is corrected by appending a compensating event ("Adjustment: -$30, reason: duplicate"), exactly like an accountant posts a correcting entry rather than erasing ink. Schema change over years is handled by upcasting: when you read an old event version, transform it on the fly into the current shape before applying it.
When it is the wrong tool
Event sourcing adds real complexity: eventual consistency in read models, replay tooling, schema/upcasting discipline, and a steeper mental model for the whole team. If an entity is simple CRUD with no audit or temporal need (a user's display-name preference), event sourcing is over-engineering. Reach for it where history is the product: ledgers, order lifecycles, inventory, anything audited.
Recap: store immutable events as truth and fold them to derive state, bound replay with snapshots, guard writes with expected-version optimistic concurrency, correct by appending (never editing), and use it only where audit/temporal value justifies the complexity.
Apply
Your turn
The task this lesson builds to.
Design an account-balance/ledger service using event sourcing: rebuild the current balance from events, support 'balance as of last Tuesday,' and handle a continuously growing event log.
Think about
- How is current state derived, and how do snapshots bound replay cost?
- How does optimistic concurrency via expected version work?
- When is event sourcing not a fit?
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Design the event-sourced core of a Stripe-style payments ledger handling 50M ledger entries/day, where every balance movement must be auditable for 7 years, double-entry invariants must always hold, and finance runs point-in-time reports for any past instant.
Think about
- How do you keep the double-entry invariant from ever being half-applied?
- How do snapshots plus tiered storage make 128B events foldable and affordable?
- How do you produce an exactly-reproducible point-in-time trial balance?