Overview
Event sourcing stores every change to an entity as an immutable event in an append-only log, instead of storing only the current state. The current state is derived by replaying events: an account's balance is the sum of its Deposited and Withdrawn events. The event log becomes the source of truth.
This gives a complete audit trail, the ability to rebuild state at any point in time, and the freedom to create new read models by replaying history. The costs are more complex querying (usually requiring CQRS projections), event schema evolution, and snapshotting to avoid replaying very long histories.
Your bank does not just store 'balance = $1,240'. It stores every deposit and withdrawal. The balance is computed from those entries, and you can always see how you got there.
When to use it
- Audit requirements: finance, healthcare, compliance.
- Domains where history matters (ledgers, order lifecycles, version control).
- Rebuilding or adding new views from past data.
- Debugging by replaying exactly what happened.
Where it shows up in interviews
Recognize it when: every money movement must be auditable.
- Design a digital wallet
- Design a payments ledger
Recognize it when: what was the state at a past time?
- Design an insurance policy system
- Design an order tracking timeline
Where it is used in real software
Commits are an event log of changes; any version can be reconstructed.
Ledgers record immutable entries; balances are derived and reconciled.
Purpose-built event stores or Kafka topics with compaction hold event streams.
Key terms
- Event
- An immutable fact in the past tense (FundsDeposited).
- Event stream
- Ordered events for one aggregate.
- Rehydration
- Rebuilding state by replaying events.
- Snapshot
- Saved state at a version to shorten replay.
- Upcasting
- Converting old event versions to the new schema on read.
How it works, step by step
- 1Load the aggregate
Read the snapshot plus later events and apply them.
- 2Handle a command
Validate against current state.
- 3Emit new events
Append with an expected version (optimistic concurrency).
- 4Publish events
Projectors update read models.
- 5Snapshot periodically
For example every 100 events.
Rebuilding an account from events
Stream for account A-1
| Version | Event | Balance after |
|---|---|---|
| 1 | AccountOpened | $0 |
| 2 | FundsDeposited $500 | $500 |
| 3 | FundsWithdrawn $120 | $380 |
| 4 | FundsDeposited $60 | $440 |
| 5 | WithdrawalRejected $900 (insufficient) | $440 |
NOWVersion: 1 | Event: AccountOpened | Balance after: $0
Current state is a fold over events. The rejected withdrawal is also recorded, which a state-only model would have forgotten.
Implementation
type AccountEvent = | { type: "AccountOpened"; accountId: string } | { type: "FundsDeposited"; amountCents: number } | { type: "FundsWithdrawn"; amountCents: number }; type Account = { balanceCents: number; version: number }; const apply = (state: Account, event: AccountEvent): Account => { switch (event.type) { case "AccountOpened": return { balanceCents: 0, version: state.version + 1 }; case "FundsDeposited": return { balanceCents: state.balanceCents + event.amountCents, version: state.version + 1 }; case "FundsWithdrawn": return { balanceCents: state.balanceCents - event.amountCents, version: state.version + 1 }; }}; export async function withdraw(accountId: string, amountCents: number) { const events = await store.readStream(accountId); const account = events.reduce(apply, { balanceCents: 0, version: 0 }); if (account.balanceCents < amountCents) throw new Error("Insufficient funds"); // Optimistic concurrency: fails if another writer appended since we read await store.append(accountId, [{ type: "FundsWithdrawn", amountCents }], { expectedVersion: account.version });}Complexity and performance
Keep it bounded.
With version check.
Trade-offs
Full history and replay come at the cost of needing projections for queries.
Deleting personal data from immutable logs requires crypto-shredding (encrypt per user, delete the key).
Variants and related techniques
Periodic saved state for faster loads.
Events carry enough data for consumers to avoid calling back.
Common mistakes
- Changing the meaning of old events.
Fix: Events are immutable; add new event types or versions and upcast.
- Using event sourcing for simple CRUD.
Fix: The complexity only pays off when history matters.
- Events that describe technical changes (RowUpdated).
Fix: Model business facts (OrderShipped).
Interview questions
What are the benefits of event sourcing?
A complete audit log, the ability to reconstruct state at any time, easy creation of new read models by replay, and a natural fit with event-driven integration.
How do you handle concurrency in an event-sourced aggregate?
Append with an expected version. If another writer appended first, the append fails, and the command reloads state and retries or rejects.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Implement an event-sourced bank account | Medium | Fold and versioning. |
| Design an event-sourced order system with projections | Hard | CQRS + snapshots. |