Module 28 / 46 · Phase F — Event-Driven at Scale · 45 min

Event
sourcing.

Stop storing the current state. Store the events that produced the state, and compute the rest. The architectural pattern behind every banking ledger, every Git repo, every audit trail worth its name. A small mental shift that quietly changes everything else.

// What you'll know by the end

  • What event sourcing actually changes
  • The event store, aggregates, and projections
  • How replay reconstructs state from history
  • When event sourcing earns its complexity tax
§ 01 — Where current state hides the truth

Your bank doesn't
have a balance.

Your bank has a ledger. The balance is what they compute from the ledger. Every deposit, every withdrawal, every fee, every interest accrual is written down, in order, never erased. Ask "what's my balance?" and they don't read a number from a column — they replay the history and tell you the sum. Every regulated financial system on earth works this way. Banks have known for centuries that storing the current state isn't enough. The events that produced the state matter more than the state itself.

And yet, the default architecture for almost every business system is the opposite. CRUD: Create, Read, Update, Delete. A row in a database represents the current state. Each update overwrites the previous value. The history of how you got here is gone — unless you specifically engineered it back in, usually with audit tables, change-data-capture, or "snapshots." This default is so universal we forget it's a choice. Event sourcing is a different choice.

// QUESTIONS YOUR CRUD DATABASE CAN'T ANSWER WITHOUT HACKS
What was the balance at 3pm on March 5th?
If you only store current state, the past is unreachable. With events, every historical state is a query — just replay up to that timestamp. Time-travel is free once your data is event-shaped.
Why is this number wrong?
Debugging in CRUD systems is forensic: comb the logs, guess at sequences. With events, you have the literal sequence of every change that led to the bug. Reproducing the bug becomes replaying the events.
Can we add a "monthly summary" feature retroactively?
In CRUD: no — you only have today's snapshot. With events, you build a new projection from the same log and backfill it from history. New features can be applied to old data without migrations.
A bug deleted a column. Can we recover?
In CRUD: maybe, if backups are recent enough. With events: yes, fully — rebuild the deleted state by replaying the log. The append-only log is the system of record. Backups are convenience, not survival.

The pattern was formalized as "event sourcing" by Greg Young and Martin Fowler in the mid-2000s, building on patterns already common in banking, accounting, and version control. Git is the most-used event-sourced system on the planet — every commit is an event; the working tree is just one projection. So is your accounting software. So is the operations log in any database. What event sourcing does is take that pattern, which was always there for high-stakes systems, and make it a deliberate architecture choice for ordinary ones.

State is derived. Events are fundamental. Once you internalize that, half of distributed-systems pain goes away.
§ 02 — The mental shift

From state
to events.

The diagrams look almost identical. The code looks similar. The mental model is entirely different. In CRUD, current state is the truth, history is a side effect. In event sourcing, history is the truth, current state is a side effect — derived by replaying events. This sounds like a small flip; it isn't. It changes what you can ask, what you can debug, what you can build on top of the system after the fact.

// SAME DATA, TWO STORAGE MODELS

// CRUD · STATE-CENTRIC
Current state
overwritten.
"Each update mutates the row. The previous value is lost."

A single accounts row. Every deposit, withdrawal, fee changes balance in place. Reading is fast (one query, one row, one number). Writing is fast (one UPDATE statement). History is a separate concern — usually solved with audit-trigger tables, change-data-capture, or "just check the logs."

-- after Deposit $500, Deposit $1200, Withdraw $300
SELECT * FROM accounts
WHERE id = 42;

-- result
{ id: 42, balance: 1400, updated: ... }

-- where did the $1400 come from?
-- the database doesn't know.
// EVENT-SOURCED · LOG-CENTRIC
Events appended
forever.
"Each change is an immutable event. State is computed by replay."

An append-only events log. Every deposit, withdrawal, fee is a new row — nothing is ever updated or deleted. Reading current state requires replaying events (cached as a "snapshot" for perf). History is the data; current state is a view over it.

-- after Deposit $500, Deposit $1200, Withdraw $300
SELECT * FROM events
WHERE aggregate_id = 42
ORDER BY seq;

-- result
[
 { seq: 1, type: "AccountOpened" },
 { seq: 2, type: "Deposited", amount: 500 },
 { seq: 3, type: "Deposited", amount: 1200 },
 { seq: 4, type: "Withdrew", amount: 300 }
]

-- balance? reduce + sum.

Notice what's not in the events table: a balance column. There's no current state stored anywhere. The balance only exists as a function over events: balance = sum(deposits) - sum(withdrawals) - sum(fees) + sum(interest). Run that function over the full event log and you have today's balance. Run it over events up to last Tuesday and you have last Tuesday's balance. Time-travel queries that would require complex audit tables in CRUD become free in event-sourced systems.

The trade is real and bidirectional. Writes get an audit trail for free; reads get more expensive — instead of one indexed lookup, you need to either replay events or cache a derived "projection." For low-write/high-read workloads (a dashboard), the projection cache hides the cost. For high-write/high-read workloads (a real-time game state), the cost is harder to swallow. This is why event sourcing isn't the default for everything — but it earns its complexity in the workloads that need its strengths.

The CRUD table answers "what is it?" The event log answers "how did it get there?" Most production bugs are the second question.
§ 03 — Anatomy of an event-sourced system

Five terms. The whole
architecture.

The conceptual machinery of event sourcing is small. Five terms cover almost everything you'll read in the literature — Greg Young's talks, Vaughn Vernon's books, the EventStoreDB docs, Marten's docs, Axon Framework's reference. Internalize these five and the rest of the topic unpacks naturally.

// THE FIVE CORE CONCEPTS · ONE DIAGRAM

EVENT-SOURCED FLOW · command → event → projection COMMAND "Deposit $500" AGGREGATE Account #42 decides whether to accept the command EVENT STORE · append-only log #1 AccountOpened #2 Deposited $500 #3 Deposited $1200 #4 Withdrew $300 #5 Deposited $500 ← new emits event PROJECTIONS read-side views CURRENT BALANCE $1,900 = Σ events TRANSACTION LIST 4 entries = map(events) MONTHLY STATS +$1700/-$300 = group + sum AUDIT TRAIL full history = events as-is
Command

A request to change state. Phrased in the imperative: DepositMoney, RegisterUser, ShipOrder. Commands can be rejected (insufficient funds, user already exists) — they aren't yet facts. They're proposals.

Aggregate

The thing the command operates on (an Account, an Order, a User). It loads its own past events, decides whether the command is valid given that history, and emits one or more new events. Aggregates enforce invariants — "you can't withdraw more than your balance" lives here.

Event

An immutable fact about what happened. Phrased in the past tense: MoneyDeposited, UserRegistered, OrderShipped. Events can't be rejected — they already happened. They're the truth of the system. The persistence model is "append, never update, never delete."

Event Store

The append-only log holding every event the system has ever emitted, grouped per aggregate. EventStoreDB, Marten (on Postgres), Axon Server are purpose-built; many teams use Postgres or Kafka as event stores with conventions. The only operations allowed are append and read.

Projection

A derived read-model built by consuming events. Each projection answers one question: "what's the current balance?", "what's the transaction history?", "what's monthly spending?". Projections can be rebuilt from scratch by replaying the log. Add a new feature? Add a new projection; backfill from history.

The flow: a command hits an aggregate, which checks invariants against its loaded event history and emits new events to the event store. Downstream, multiple projections consume those events to build the read-models the rest of the system queries. Commands write; projections read. The event store is the single source of truth in between. This shape — separating write from read with an immutable log between them — is exactly what CQRS (the next module) generalizes.

Commands ask. Events tell. Aggregates decide. The event store remembers. Projections answer.
§ 04 — Event store simulator · interactive lab

Same events.
Three views of reality.

Below: a bank account modeled as an event-sourced aggregate. Click the action buttons to append events to the log. Watch all three projections — Balance, Transaction History, Monthly Stats — update live from the same source. Then drag the time-travel slider backward to see the account's state at any past point. The events don't move; only the cutoff does.

EVENTSTORE.SIM // m.28 lab
// COMMANDS:
// TIME TRAVEL: all events
// EVENT STORE · append-only log (read →) 10 events · viewing all
// PROJECTION · CURRENT BALANCE
Current Balance
$0
computed from 0 events
// PROJECTION · TRANSACTION HISTORY
Transaction History
// PROJECTION · MONTHLY STATS
Account Stats
§ 05 — When event sourcing pays off (and when it doesn't)

A real trade.
Not always worth it.

Event sourcing is a powerful tool. It's also a complexity tax — pay it on the wrong workload and you've made the system harder for no reward. Greg Young, who named the pattern, has been the loudest voice arguing it's over-applied: most CRUD systems would suffer if migrated, not benefit. The right question isn't "is event sourcing good?" but "does this particular domain reward the trade-offs?"

// THE HONEST LEDGER · WHAT YOU GAIN AND PAY

Audit trail free
Every change is permanently recorded. "Who did what when?" is a query, not a feature you have to design and maintain. Regulated industries (finance, healthcare, government) get this for free instead of bolting on audit tables.
Time travel built in
Any past state is reachable. Reproduce a bug exactly as it happened. Answer "what was X at time T" with a replay. The state of the system is a pure function of events; the function takes a time parameter.
New projections, retroactively
New questions can be asked of old data. Build a new read model; backfill it from the event log. The historical data was never thrown away, so features that need history (recommendations, fraud detection, analytics) can incorporate years of past behavior without migration projects.
Aligns with the business
The events of the system match the events of the business. "Order placed", "payment received", "shipment dispatched" — these are how the domain experts think. Event-storming workshops (Alberto Brandolini) use this directly: stakeholders sticky-note the events of a business process, and the architecture falls out.
Schema evolution is hard
You can't change historical events. They were written in v1; they have to be readable in v100. Strategies: weak schemas (JSON), versioned event types (UserRegistered_v2), upcasters that transform old events on read. Every long-lived event-sourced system has scar tissue around this.
Read latency higher (without projections)
Replaying events is slower than reading a row. Solved by caching projections — but now you have a cache, with its invalidation problems. Snapshots (periodic full-state caches in the log) help; they don't eliminate the cost.
Eventual consistency between writes and reads
Projections lag the event store. A user deposits money; the balance projection might be 50ms behind. For some domains (banking) this is fine; for others (real-time leaderboard) it's a UX problem. Mitigation: read your own writes from the event store directly, not the projection.
Teams need to think differently
Engineers used to CRUD struggle initially. "Where do I save the user?" doesn't have a simple answer. Onboarding takes longer; pull-request reviews catch more subtle issues. The complexity is real — and it doesn't go away just because the pattern is "elegant."

The honest summary: event sourcing fits well when history matters as much as state — finance, healthcare, supply chain, audit-heavy systems, anything where regulators or accountants will eventually ask "what happened in October?" It fits poorly for ephemeral state (cache layers, real-time chat presence), high-write low-history domains (sensor telemetry where you just want the latest reading), and small teams without the bandwidth to absorb the learning curve.

A common middle path: event sourcing for the core domain, CRUD for the periphery. The accounting / order / inventory parts of an e-commerce system are event-sourced; the user-profile / preferences / search-index parts are CRUD. You don't have to pick one for the whole system. The next module — CQRS — formalizes this by separating write models from read models even more deliberately, and shows how event sourcing and CQRS pair naturally.

Event sourcing is not free. It's powerful where it pays off, baggage where it doesn't. The skill is knowing which.
§ 06 — Eight words for the event-sourcing conversation

Vocabulary,
for the log.

The terms every event-sourcing tutorial, every EventStoreDB doc, every DDD book assumes you already know.

Event Store
/ɪˈvɛnt stɔːr/
The append-only log holding every event the system has ever emitted. Purpose-built (EventStoreDB, Axon Server) or built on Postgres/Kafka. The single source of truth.
Aggregate
/ˈæɡ.rə.ɡət/
A domain object (Account, Order) that loads its own events to rebuild state, validates incoming commands against invariants, and emits new events. The unit of consistency in event-sourced systems.
Command
/kəˈmænd/
A request to change state, in the imperative (DepositMoney, RegisterUser). Commands can be rejected by aggregates if they violate invariants. Commands aren't yet facts.
Event
/ɪˈvɛnt/
An immutable fact about something that happened, in the past tense (MoneyDeposited, OrderShipped). Events can't be rejected — they already occurred. They are appended forever.
Projection
/prəˈdʒɛk.ʃən/
A derived read-model built by replaying events. Each answers a specific question (current balance, transaction list, monthly stats). Rebuildable from scratch by replaying the log.
Snapshot
/ˈsnæp.ʃɒt/
A periodic cache of an aggregate's computed state, stored in the event log alongside events. Avoids replaying thousands of events on every read; instead, load latest snapshot + events since.
Upcaster
/ˈʌp.kæs.tər/
A function that transforms an old event version into a newer one on read. Required for schema evolution in long-lived event stores, since historical events themselves are immutable.
Event Sourcing
/ɪˈvɛnt ˈsɔːr.sɪŋ/
The pattern itself: storing the log of changes as the authoritative state, with current state derived by replay. Distinct from "publishing events" — that's just notifications. Event sourcing requires the log to BE the source of truth.
§ 07 — Knowledge check

Five questions.
Replay the log.

Test the event-sourcing intuition. Click an answer; explanation drops in instantly.

QUESTION 1 OF 5
Loading question...
Score: 0 / 5
5 / 5

Sourced.

Perfect. Commands, events, aggregates, projections — the whole pattern. Now: separating the read side from the write side, deliberately.

§ 08 — The recap

Three ideas to
carry forward.

The mental shift that changes how you reason about state, history, and recovery.

i

State is derived

Events are the truth; state is a function over events. Internalize this and time-travel, audit, and replay all become trivial.

ii

Five concepts cover it

Command, aggregate, event, event store, projection. The whole vocabulary of every event-sourcing system fits in those five terms.

iii

It's not free

Schema evolution, projection lag, learning curve. Event sourcing earns its complexity where history matters; it's baggage where it doesn't.

↓ UP NEXT

M.29 — CQRS.

One model for writing, a different model for reading. Event sourcing made the separation natural; CQRS makes it explicit. The pattern that lets your write-side handle complex business rules while your read-side is tuned for the actual queries users run.

Continue to Module 29 →