Home › High-Level Design › Event Sourcing
E

Event Sourcing

Most interview answers store the current state of the world. Event sourcing stores how the world got there, and that one shift changes almost every other decision you make. I have watched candidates light up when they first get it, then crash into the same three problems in production. Let me save you that trip.

High-Level DesignAdvanced11 min read

If you remember five things

The whole concept in one glance. Everything below defends these five points.

  1. Log is truth. You store the facts that happened, not the current value. State is a function you compute by replaying the log.
  2. Writes are append-only. No in-place update, no lock on a hot row. Every write is a single sequential insert.
  3. Reads need projections. Replaying the log on every read is dead on arrival. You serve reads from a maintained read model and keep snapshots to cap replay.
  4. CQRS is the natural partner. The log is your write model, projections are your read models, and they scale on different axes.
  5. It is a specialist tool. Reach for it when history IS the product (ledgers, audit, compliance). Otherwise CRUD plus an audit table wins.

1The core idea#

Normal systems keep the answer. Event sourcing keeps the questions. That is the whole flip.

  • CRUD keeps the current value. Your bank row says balance = 420. Spend 50 and the system updates the row to 370. The old number is gone forever. As far as the database is concerned, it never existed.
  • Event sourcing keeps the facts. You never store the balance. You store a list of things that happened: AccountOpened, Deposited 500, Withdrew 80, Withdrew 50. The balance is something you compute by replaying that list.
  • State becomes a function of history. Replay the exact list on any machine and you get 500 - 80 - 50 = 370, every time.

The one line to anchor on:

  • The log of events is the source of truth.
  • Everything else, including the "current balance" on the screen, is a derived view you can throw away and rebuild.

Two properties fall out immediately, and interviewers love both:

  • Nothing is ever lost. An update is just another event appended to the end. Ask "what was the balance on March 3rd at noon" and you replay events up to that timestamp. Try that with a plain UPDATE.
  • Writes are append-only. You only ever add rows to the end of a log. No in-place mutation, no update lock on the hot row. The write is a single sequential insert, not a read-modify-write on a contended row.
The one sentence that defines the pattern

State is a left fold over an immutable log of facts. If you can say that and mean it, the snapshots, projections, and CQRS all become obvious consequences rather than separate ideas to memorize.

The trade-off hits just as fast: reading the current state now costs you a replay. Section 2 deals with it.

2How it works#

Four moving parts, and the interview punishes you for blurring them: the command, the event, the event store, and the projection.

  • Command. A request to change something: "transfer 50 dollars." A command can be rejected. The domain logic looks at current state, decides if it is valid, and emits events if so. TransferMoney is a command.
  • Event. A fact in the past tense. It already happened, so it cannot be rejected or deleted. MoneyTransferred is an event.
  • Event store. An append-only log, ordered per entity (an "aggregate" in DDD terms, like one bank account). Appending is the only write operation.
  • Projection (read model). Consumes the event stream and builds whatever shape your reads need: a balance table, a search index, a cache. Many projections can feed off the same events, each tuned for a different query.
Red flag that fails you here

Mixing "command" and "event" in the same breath, or writing an event called UpdateBalance. That is a tell you have read the blog posts but never shipped one. Commands are requests in the imperative; events are facts in the past tense. Say MoneyWithdrawn, never WithdrawMoney, when you mean the thing that already happened.

flowchart LR A[Client] --> B[Command Handler] B --> C[Event Store] C --> D[(Append Only Log)] C --> E[Projector] E --> F[(Read Model DB)] E --> G[(Search Index)] H[Query API] --> F
The write path and the read path are now separate pipelines. Notice the Query API never touches the log: it reads only the projection. The log is for truth and rebuild, the projection is for speed.

Why this pairs with CQRS.

  • The separate write and read pipelines are exactly what CQRS (Command Query Responsibility Segregation) asks for.
  • They are not the same thing, but they fit like a hand in a glove: CQRS says split your write model from your read model, event sourcing hands you a natural write model (the log) plus a natural way to feed any number of read models (projections off the stream).

The obvious problem: an account open for ten years has 40,000 events. You are not replaying all of them on every balance check. Two fixes:

  • Snapshots. Every N events, write the computed state to the side. To get current state, load the latest snapshot and replay only the events after it. Replaying 50 events is cheap. Replaying 40,000 is not.
  • Maintained projections. Keep the read model continuously up to date as events arrive, so a balance read is a normal indexed lookup. This is what most real systems do. The replay-from-scratch path is reserved for rebuilding a projection, not for serving live reads.
sequenceDiagram participant C as Client participant H as Command Handler participant S as Event Store participant P as Projector participant R as Read Model C->>H: TransferMoney 50 H->>S: load events for account S-->>H: event history H->>H: validate balance H->>S: append MoneyTransferred S-->>P: new event P->>R: update balance view C->>R: GET balance R-->>C: 370
The write flow end to end. The validation reads history, the append is the only write, and the final GET balance hits the read model, not the log. Truth and audit live in the log; speed lives in the projection.

3A worked example with real numbers#

"Replay the log" stays abstract until you put a latency number on it. Say I am designing the balance service for a neobank with 10 million accounts.

The event stream for one account. Account acc_7781 has these events, each roughly 300 bytes on disk after serialization:

seq 1  AccountOpened          balance effect  +0
seq 2  Deposited      50000   (500.00)        +50000
seq 3  Withdrew        8000   ( 80.00)        -8000
seq 4  Withdrew        5000   ( 50.00)        -5000
current balance = 37000 cents = 370.00
  • I store money in integer cents, never floats.
  • 50000 - 8000 - 5000 = 37000, which is 370 dollars.
  • Replaying 4 events takes microseconds.

Now scale it in time.

  • This account is active for 10 years and averages 11 events a day (card swipes, interest postings, fees). That is roughly 40,000 events.
  • Each replay reads 40,000 rows at 300 bytes each, so about 12 MB of I/O just to answer "what is the balance."
  • At, say, 1,000 balance checks per second across hot accounts, naive replay is dead on arrival.

Fix it with a snapshot.

  • I snapshot every 500 events. To read the balance I load snapshot number 80 (state after seq 40,000) and replay the 0 to 499 events since.
  • Worst case replay is 499 events, about 150 KB, versus 12 MB. That is an 80x cut in read I/O, and it stays flat as history grows.
  • The snapshot itself is one small row: {account, seq: 40000, balance: 37000}.

Fix it better with a maintained projection.

  • I keep a balances table updated as each event lands: one indexed row per account.
  • GET balance is a single primary-key lookup around 1 ms.
  • The 40,000-event log sits untouched behind it, used only for audit and for rebuilding the projection if it ever gets corrupted.
Pause before you read on: that 120 TB log sounds terrifying for a balance service. Is it actually a problem?
Reveal the answer
No, because it is append-only and cold: 99% of it is never read. The hot read path is the small projection, not the log. This is the whole shape of event sourcing: a large, cheap, immutable log plus small, hot, derived views. If you flinch at the log size you have misread which part serves live traffic.

Sizing the whole system.

  • 10M accounts times 40,000 events times 300 bytes is about 120 TB of event log over a decade.
  • That sounds scary until you remember it is append-only and cold: 99% of it is never read.
  • The hot read path is the balances projection: 10M rows at maybe 80 bytes, under 1 GB, fits in memory on one box.
  • The shape of the whole trade: a large cheap immutable log plus small hot derived views. Say those numbers in an interview and you stop sounding theoretical.
~120 TB
The log, and why it is fineTen years of events for 10M accounts. It is append-only and cold, so it never touches the hot read path. Reads are served by a sub-1 GB projection. This split is the entire point of the pattern.

4Trade-offs#

This is where interviews are won or lost. Anyone can describe the pattern. You get hired for knowing what it costs.

Dimension Event sourcing Classic CRUD
Source of truth Append-only event log Current-state rows
Audit and history Free, built in Needs extra audit tables
Point-in-time state Replay to any moment Lost on every update
Read performance Needs projections or snapshots Direct, simple
Write model Simple appends, no update locks In-place updates
Schema change Hard, old events are immutable Run a migration
Operational complexity High Low
Debuggability Replay the exact sequence Guess from current state

The three things you actually give up:

  • Query complexity. You cannot SELECT * FROM accounts WHERE balance > 1000 against a log. There are no rows with balances in it. Every query you want has to be served by a projection you built ahead of time. If a product manager asks for a new report next week, you may need a new projection and a full replay to populate it. Against the 120 TB log, that replay is a real job you have to plan, not a one-liner.
  • Schema evolution of events. Events are immutable and they live forever. Ship OrderPlaced with price as an integer in cents, then three years later want it as a decimal with currency, and you cannot rewrite the old events. You carry versioned event types and "upcasters" that translate old shapes to new ones on read. This is the single most underestimated cost. Teams are usually fine for a year, then event versioning becomes a tax on every change.
  • Eventual consistency. The projection lags the log, even if only by milliseconds. A user transfers money, the event is committed, but the balance view has not updated yet when they refresh. For a bank that is a lawsuit. For a social feed it is invisible. Know which side of that line your system sits on.
Reach for it when
Event sourcing
History
Is the product: ledgers, audit, compliance.
Audit trail
Free, it is the primary data.
Point-in-time
Replay to any moment in the past.
Cost
High operational complexity, event versioning tax, eventual consistency.
Default to this
CRUD plus audit table
History
A side concern you log separately.
Audit trail
Needs a deliberate audit table.
Point-in-time
Only what you chose to snapshot.
Cost
Low. Solves 80% of what people think they need event sourcing for.

My honest take:

  • Event sourcing is a specialist tool. Reach for it when history IS the product (ledgers, audit, compliance, anything where "why is it in this state" matters as much as the state).
  • Do not reach for it because it sounds sophisticated.
  • A plain CRUD table with a separate audit log solves 80 percent of what people think they need event sourcing for, at a fraction of the operational pain.

Saying no to your own pattern is the clearest senior signal in the room.

5Where it is used#

A few canonical fits, and what each one teaches:

  • Financial ledgers. The canonical case. A ledger IS an event log by definition: you never edit a posted transaction, you post a correcting one. Accounting has worked this way for 500 years, so event sourcing is software catching up to double-entry bookkeeping.
  • Order and fulfillment systems. An order moves through placed, paid, picked, shipped, delivered, returned. Each transition is a meaningful event that downstream systems (inventory, analytics, customer notifications) all want to react to. The event stream becomes the integration backbone.
  • Version control. Git is event sourcing in disguise. Commits are immutable events, your working tree is a projection, and you can check out any point in history by replaying. If you understand Git, you already understand the pattern.
  • Collaborative editing and distributed systems. When you merge changes from multiple sources or rebuild state on a new node, a stream of ordered events is far easier to reason about than reconciling two bags of mutable state.

The signal words that should make you reach for it:

  • "audit," "regulatory," "undo/redo," "temporal query," or "replay for debugging."
  • If the prompt is "design a URL shortener," event sourcing is the wrong answer and reaching for it shows poor judgment.

6Common pitfalls and gotchas#

The traps that separate someone who ran one of these from someone who read about it:

  • Treating events as commands. New teams write events like UpdateBalance that carry the new value. That is a disguised CRUD update. A real event says what happened and why (MoneyWithdrawn 50), not what the row should become. Get this wrong and you lose the ability to re-derive state under new rules later.
  • Fat events that leak current state. If your event stores the resulting balance (balance now 370) instead of the delta (withdrew 50), you have coupled the event forever to how you computed balance at that moment. Store the fact, derive the state. In the section 3 example, storing balance: 37000 inside the Withdrew event would bake a bug in old balance math permanently into history.
  • No snapshot strategy. Day one, replays are instant because there are ten events. Eighteen months later a single aggregate has 100,000 events and your p99 latency quietly dies. Plan snapshots before you need them, not after the pager goes off.
  • Ignoring idempotency on the read side. Projectors consume events, and in a distributed system they will occasionally see the same event twice (at-least-once delivery). If your projector does balance = balance + amount without tracking which events it already applied, a redelivery double-counts that 50 and corrupts the view. Track the last processed event position per projection and skip anything at or below it.
  • Deleting data in a GDPR world. Immutable logs and "the user has a right to be forgotten" are in direct conflict. You cannot just delete events. The usual answer is crypto-shredding: encrypt personal data in events with a per-user key, then delete the key. The events remain but the PII is unrecoverable. Bring this up unprompted and interviewers notice.
  • Rebuilding a projection takes hours. If a projection bug ships and you need to rebuild from a billion events, that replay can run for a long time. Teams that care keep projections rebuildable in parallel and partitioned by account, so a rebuild is minutes, not a weekend.
From the interviewer's chair

I throw the crypto-shredding question at candidates who sound too comfortable. If you name "immutable log versus right-to-be-forgotten" as a real tension and reach for per-user-key encryption plus key deletion before I prompt you, I mark you as someone who has operated one of these in a regulated shop. Reciting the happy path does not do that.

7How it shows up in interviews#

Interviewers probe event sourcing to see if you understand consistency, not just vocabulary. Here is what separates strong from weak, framed as the questions you will actually get.

Follow-ups the interviewer will ask (and your answers)
"How do you read the current balance?"
Serve reads from a maintained projection, which is a single indexed lookup around 1 ms. Use snapshots every 500 events to cap worst-case replay, and keep the log only as the source of truth and for audit or rebuild. The weak answer, "replay all the events," ignores that the account in my example has 40,000 of them.
"A user says the balance is wrong right after a transfer."
That is the eventual-consistency window between the committed event and the updated projection. For a banking balance I would either read-your-own-writes by serving the writer's session from the write model, or make that specific read strongly consistent and accept the latency. For a non-critical view I would leave it eventual.
"How do you change an event's schema?"
Versioned events plus upcasters that transform old versions to the current shape on read. I never rewrite the log. I accept that old events are permanent and design them lean from the start. You cannot migrate immutable history, and there are 120 TB of it.
"A projector sees the same event twice. What happens?"
Nothing, if I built it right. I track the last processed event position per projection and skip anything at or below it. Without that, an at-least-once redelivery double-counts the amount and corrupts the view. Idempotency on the read side is not optional.
"When would you NOT use this?"
If there is no audit or temporal requirement, event sourcing adds operational cost for no benefit. I would use CRUD with an audit table unless history is a first-class product requirement. A URL shortener should never be event sourced.

The meta-move: mention CQRS and eventual consistency early, draw the split read/write diagram, and name snapshots and event versioning before you are asked. That tells the interviewer you have actually run one of these, not just read about it.

What you can now explain in the room

  • Why the log is the source of truth and current state is just a derived view you can rebuild.
  • Why reads need a maintained projection and snapshots instead of replaying the whole log.
  • How event sourcing and CQRS fit together without being the same thing.
  • The real numbers: a 120 TB cold log behind a sub-1 GB hot projection, and why that is fine.
  • The three costs: query complexity, event schema evolution, and eventual consistency.
  • When to say no to event sourcing and reach for CRUD plus an audit table instead.

Next, try the System Design Question walkthrough on designing a payment ledger, where these trade-offs stop being theory and you have to pick a side.

Get a prep plan built around your Event Sourcing interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Event Sourcing prep plan is on the way.