Skip to main content

Event Sourcing

Level 6: Level 6: Asynchronous & Event-Driven Systemshard30 minevent-sourcingledger

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).

Check yourself
An auditor asks for Account #42's balance as of last Tuesday at 5pm. The event-sourced ledger folds events up to that timestamp and answers. Why can a mutate-in-place system not answer the same question?

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.

Check yourself
Last month's Deposited 100 event should have been 70. What is the event-sourced correction?

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.

Check yourself
An operator accidentally deletes every snapshot for a 10-year-old account with 100k events. What is permanently lost?

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

  1. How is current state derived, and how do snapshots bound replay cost?
  2. How does optimistic concurrency via expected version work?
  3. 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

  1. How do you keep the double-entry invariant from ever being half-applied?
  2. How do snapshots plus tiered storage make 128B events foldable and affordable?
  3. How do you produce an exactly-reproducible point-in-time trial balance?