Picture a bank account row in a database. It says the balance is $40. That single number is true, but it's also strangely empty: was it always $40? Was it $1,000 yesterday? Did someone withdraw $960, or were there forty separate deposits of a dollar? The row can't tell you, because every update overwrote the one before it.
Event Sourcing is the idea that you should keep the story, not just the ending — record every change that ever happened, and treat that history as the real source of truth.
The problem
The default way we store data is update-in-place: there's a row for each thing, and when something changes we overwrite the relevant columns with the new values. A transactional store like this is simple and fast to query — the current state is sitting right there — but it quietly throws away everything that isn't current.
That loss bites in three ways. You have no real audit trail, because the old values are gone the instant they're replaced. You can't answer temporal questions like "what did this customer's cart look like last Tuesday?" — there's no last Tuesday to look at. And you lose the "why": you can see that the balance is $40, but not the deposit, the purchase, and the refund that produced it. Bolting on history columns and audit tables after the fact is fiddly, easy to get wrong, and never quite complete.
How it works
Event Sourcing flips the relationship between state and history. Instead of storing the latest state and discarding the changes, you store the full, ordered sequence of events that happened — MoneyDeposited, MoneyWithdrawn, RefundIssued — in an append-only log. Each event is immutable: once written it is never edited or deleted, only new events are added after it.
So where does the current balance come from? You replay the events. Starting from nothing, you apply each event in order — add the deposit, subtract the withdrawal, add the refund — and the value you're left with is the current state. The log is the source of truth; the current state is just a calculation over it. And because you can stop replaying at any point, you can reconstruct the exact state of the account at any past moment — a kind of time travel that update-in-place can never offer.
- Event logAn append-only record of every event; current state is derived by replaying it.
- Read modelA projection built by replaying events, shaped for queries.
Commands and events are different things. A command is a request to change something — "withdraw $50" — and it can be rejected (insufficient funds). An event is a fact about something that already happened — "$50 was withdrawn" — and facts can't be argued with. The log only ever contains events, which is exactly why it's safe to treat as immutable history.
What you get for it
The payoff starts with a complete audit trail that you don't have to engineer — it's a side effect of how the system stores data. Every change is recorded, in order, with whatever context you chose to capture, and nothing is ever silently overwritten. For domains like finance, healthcare, or anything regulated, that's enormously valuable.
From the same log you get state reconstruction at any point in time, which makes debugging ("replay up to the moment it broke") and analytics ("what did things look like at quarter-end?") straightforward. And because each event is a clean, meaningful record of something that happened, those same events can feed other systems — you can publish them so other services react, or build entirely new views of the data later by replaying history through new logic.
It rarely travels alone
An append-only log is wonderful for writing and terrible for asking "what's the balance right now?" — you'd have to replay everything every time. This is why Event Sourcing is almost always paired with CQRS: the events on the write side are used to build separate, query-optimized read projections (a plain accounts table with a balance column, say) that the read side serves quickly. The log stays the source of truth; the projection is a derived, disposable view you can always rebuild.
The events are also a natural fit for pub/sub: as each event is appended, you publish it to subscribers, so other parts of the system react to changes without the writer knowing who's listening. Together these three patterns describe a common event-driven shape — write events, project them into read models, and broadcast them to whoever cares.
The trade-offs
None of this is free, and Event Sourcing is genuinely harder than an update-in-place table.
- Querying current state takes work. You can't just
SELECTthe balance from the log; you need projections to read from, or snapshots (a periodically saved checkpoint of state) so replay doesn't have to start from the very first event every time. - The read side is eventually consistent. Projections are built from events after they're written, so for a brief window the read model lags behind reality — the same trade-off CQRS and pub/sub make.
- Schema versioning is a real challenge. Old events are immutable and live forever, so when the shape of an event changes you can't go back and rewrite them. Your replay code has to understand every historical version of every event, or you need an upcasting strategy to translate old events into the new shape on the way in.
The log only grows — design for that from day one. Events are never deleted, so an event store accumulates forever, and naive replay gets slower as history lengthens. Plan snapshots, retention and archival, and versioning before you go to production. Retrofitting them onto a log that's already millions of events deep is painful.
When to use it
Reach for Event Sourcing when history is part of the requirement, not an afterthought: when you need a trustworthy audit trail, when answering "how did we get here?" matters, or when you want to derive new views of past data you didn't anticipate. Domains with rich, meaningful state changes — finance, ordering, logistics, anything with a strong notion of what happened — tend to fit it well.
For a simple CRUD app where the current state is all anyone ever needs, it's overkill — a plain transactional update-in-place store is simpler, faster to query, and easier to reason about. Like most architectural patterns, Event Sourcing buys you audibility, temporal insight, and integration leverage in exchange for added complexity, eventual consistency, and the long-term burden of versioning your events.