Skip to content

Event Sourcing

Storing the sequence of changes instead of the current state — what it buys, the schema evolution problem, and why it is hard to reverse.

Advanced18 minUpdated 8 August 2026

Event sourcing stores every change that happened rather than the current state. The state is not stored at all — it is derived by replaying the events from the beginning.

It is a genuine and occasionally transformative pattern, and it is also the hardest architectural decision in this module to reverse. A system with three years of events cannot go back to storing state without a migration that reinterprets every one of them. That asymmetry deserves more weight than it usually gets.

What changes

// State-oriented: the current value, history lost
{ id: 42, status: 'shipped', total: 2999, address: '12 New Road' }

// Event-sourced: what happened, in order
OrderPlaced   { id: 42, items: [...], total: 2999 }
AddressChanged{ id: 42, from: '3 Old Street', to: '12 New Road' }
PaymentCaptured{ id: 42, amount: 2999 }
OrderShipped  { id: 42, carrier: '…', at: '…' }
Conceptual diagramnoyesCommand:ShipOrderAggregate:load by replaying eventsValid givencurrent state?RejectAppend OrderShippedto the event logEvent log —append onlySnapshotsevery N eventsProjectionsread models
Diagram, as text
  • Command: ShipOrder leads to Aggregate: load by replaying events
  • Aggregate: load by replaying events leads to Valid given current state?
  • Valid given current state? leads to Reject (no)
  • Valid given current state? leads to Append OrderShipped to the event log (yes)
  • Append OrderShipped to the event log leads to Event log — append only
  • Event log — append only leads to Snapshots every N events
  • Event log — append only leads to Projections read models

The event log is append-only and immutable. Nothing is updated and nothing is deleted; a correction is a new event.

What it genuinely buys

A complete, truthful audit trail. Not a log written alongside the state and hoped to be accurate — the events are the state, so the audit cannot drift from reality. For finance, healthcare and anything regulated, this alone can justify the pattern.

Answers to questions nobody asked yet. "What did this look like on the 3rd of March?" and "how many orders had their address changed after payment?" are answerable from data you already have. A state-oriented system would need to have anticipated the question.

Debugging by replay. Reproduce a bug by replaying the exact events, in a test, with no attempt to reconstruct the state that caused it.

New read models from history. A new projection can be built over all past events, so a new feature starts with full history rather than from its deployment date. This is a striking capability and it is frequently the thing that sells the pattern.

Natural fit with CQRS. The event log is a ready-made source for projections — which is why the two are so often taken together, and why it is worth remembering they are separate decisions.

What it costs

Schema evolution is permanent. Events from three years ago must still be readable by today's code. You cannot migrate them the way you migrate a table — or rather, you can, and doing so destroys the immutability the pattern rests on. Every event type is a contract with your own future.

The workable approaches: upcasting — a function that converts an old event version into the current shape on read — or weak schemas, where consumers tolerate missing fields. Both need discipline from the first event, and retrofitting either is painful.

Replaying gets slow. An aggregate with 50,000 events cannot be rebuilt per request. Snapshots — a stored state at event N, replaying only from there — are mandatory beyond a modest event count, and they are themselves versioned state that must be invalidated when logic changes.

Deleting data is architecturally awkward. "Delete this user's data" against an append-only log is genuinely hard. The usual answer is crypto-shredding: encrypt personal data per subject and delete the key, rendering the events unreadable without mutating them. This must be designed in from the start; retrofitting it means rewriting the log, which is exactly what the pattern says you cannot do.

Everyone must understand it. Current state is not queryable directly, "just look at the row" is not available, and every new engineer needs the model explained. This is a real ongoing cost.

Eventual consistency in every read, since reads come from projections.

Events versus commands, and the correction rule

A command is a request that may be rejected. ShipOrder — the aggregate validates against current state and decides.

An event is a fact that already happened. OrderShipped — it cannot be rejected, only recorded. Events are always past tense, and a "rejected event" is a contradiction that signals the model is confused.

A mistake is corrected by appending, never by editing. If an order was shipped in error, that is ShipmentReversed, not the deletion of OrderShipped. This feels awkward and it is the point: the history says what really happened, including the mistake, which is precisely what an audit trail is for.

Check your understanding

Common mistakes

Event-sourcing everything. Permanent cost on aggregates with no audit value.

Adopting it together with CQRS as one decision. Two unfamiliar architectures at once.

No versioning strategy from the first event. Retrofitting upcasting across years of history is painful.

No snapshots. Replaying tens of thousands of events per request is unworkable.

Editing or deleting events. It destroys the property the whole pattern rests on.

Events named as commands. ShipOrder in a log is a confusion about what has already happened.

No plan for personal-data deletion. Crypto-shredding must be designed in, not retrofitted.

Storing derived values in events. A total computed by logic that later changes makes history inconsistent with itself; store the inputs.

Practice

A fintech team wants to event-source their entire platform: accounts, transactions, user profiles, notification preferences and the product catalogue. Advise them.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Sort the five by whether anyone will ever ask what changed and when.

Show a strong answer

Sort by audit value, because that is what the pattern is paid for.

  • Accounts and transactions: yes, clearly. Money movement is the canonical case. A regulator, an auditor or a customer dispute will ask exactly the questions an event log answers, and the alternative — a state table plus a hand-maintained audit log — has the failure mode that the two drift and nobody notices until the drift matters.
  • User profiles: probably, narrowly. "Who changed this address and when" is a genuine fraud-relevant question in a financial context. But a profile also holds a display name and an avatar, where history is worthless. I would event-source the security-relevant fields and store the rest normally, rather than treating "profile" as one thing.
  • Notification preferences: no. Nobody will ask what someone's email preference was in March. The current value is the whole requirement, and event-sourcing it means permanent schema obligations for a boolean.
  • Product catalogue: no. It is reference data, frequently synchronised from elsewhere, and its history — if wanted at all — is better served by versioned records than by an event log.

So my advice is: event-source accounts and transactions, plus the security-relevant profile fields. Store the rest conventionally. Mixed storage in one system is normal and is not a compromise to apologise for.

What I would insist on before the first event is written, because all of these are painful to retrofit:

  • A versioning strategy — upcasting functions from day one, with a test that every historical version still loads. In fintech, events will outlive several rewrites of the code.
  • Crypto-shredding for personal data. Financial systems carry deletion obligations, and an append-only log cannot satisfy them by deleting. Encrypt per subject and delete the key. Designed in, this is straightforward; retrofitted, it means rewriting the log.
  • Snapshots, since a busy account accrues events quickly and per-request replay will not survive contact with production.
  • A projection rebuild path, tested, before it is needed in an incident.

What I would warn them about explicitly: this is the least reversible decision in the architecture. In three years, moving accounts back to state-oriented storage means reinterpreting every event ever written. That is an argument for starting narrow — one aggregate, in production, for a quarter — before extending. If it suits the team, extending is easy; if it does not, they have one aggregate to unwind rather than a platform.

One thing I would push back on hard: the word "entire". A team proposing to event-source everything usually has not yet met upcasting, snapshot invalidation or a deletion request, and those three are where the cost actually lands.

Design the versioning strategy for an event-sourced system, covering: adding a field, removing a field, renaming a field, splitting one event into two, and changing the meaning of a field.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

For each, ask what happens when code from today reads an event from three years ago.

Show a strong answer

The governing rule: old events are immutable, so all compatibility work happens on read. Every case below is answered by asking what today's code does when it encounters a three-year-old event.

Adding a field. Safe. Old events lack it, so the reader supplies a default — but the default must be semantically correct for the era, not merely a zero value. A currency field added to an old PaymentCaptured should default to whatever the system actually used then, not to empty. Getting this wrong produces history that is quietly false, which is worse than history that is missing.

Removing a field. Stop writing it; keep reading it. The upcaster drops it when converting old events to the current shape. Never reuse the name for anything else — a replay would misread old events as carrying the new meaning, and nothing would fail.

Renaming a field. Purely a read-side mapping: the upcaster maps total to amountInCents. This is the easiest case and the one people over-think, because the instinct is to migrate the stored events. Do not.

Splitting one event into two. The most involved case. OrderPlaced becoming OrderPlaced plus PaymentAuthorised means an upcaster that turns one historical event into two — which is legitimate, because the upcaster produces the in-memory representation rather than rewriting storage. The hard part is that the derived second event must carry plausible values for fields the original never had, and if it cannot, the split is not achievable this way and the aggregate needs to handle both shapes directly.

Changing the meaning of a field. Never do this. It is the only case with no safe mechanism. If amount meant units and now means cents, every old event is off by a hundred and nothing fails — no error, no exception, just wrong numbers that will be discovered at an audit. Add a new field with a new name and leave the old one alone.

The machinery that makes this real rather than aspirational:

  • A version number in every event from the first one. Retrofitting a version to unversioned events means inferring it from shape, which is guesswork.
  • Upcasters as a chain, v1→v2→v3, so a new version adds one function rather than editing every existing one.
  • A test that loads a stored sample of every historical version and asserts the current shape. This is the control that keeps the whole scheme honest, and it must run in CI, because the failure mode otherwise appears only during a replay months later.
  • Sample events kept in the repository, not generated. Generated samples reflect today's writer and would pass even if the upcaster were broken.

What I would document alongside the code: for each version bump, why it happened. Three years on, an upcaster with no explanation is a function nobody dares change.

Key takeaways

  • Event sourcing stores the sequence of changes, deriving state by replay; the log is append-only and immutable.
  • It buys a truthful audit trail, answers to questions nobody asked yet, debugging by replay, and new read models built over full history.
  • It is the least reversible decision here. Three years of events cannot be turned back into state without reinterpreting all of them.
  • Schema evolution is permanent work — upcasting or tolerant readers, versioned from the very first event.
  • Snapshots are mandatory beyond a modest event count, and they are versioned state that must be invalidated when logic changes.
  • Personal-data deletion needs crypto-shredding designed in from the start.
  • Corrections are new events, never edits — the mistake stays in the history, which is the point.
  • Never change a field's meaning, and never event-source aggregates with no audit value.

Further reading

  • Greg Young, "Event Sourcing" and the CQRS Documents (2010).
  • Martin Fowler, "Event Sourcing" (2005).
  • Vaughn Vernon, Implementing Domain-Driven Design (Addison-Wesley, 2013) — event-sourced aggregates and snapshots.
  • Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017) — logs, change data capture and derived state.
  • Pat Helland, "Immutability Changes Everything" (CIDR, 2015).
  • Alexey Zimarev, Hands-On Domain-Driven Design with .NET Core (Packt, 2019) — event versioning and upcasting in practice.

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonCircuit Breaker
Lessons in this track