Web Development · Architecture Patterns
Event Sourcing Explained: Storing What Happened, Not Just What's True Now
Event sourcing stores every change to your data as an immutable event, then derives current state by replaying them, instead of overwriting a row each time something changes. Here's what that actually buys you, and the real operational cost it isn't worth paying in most systems.
Abhishek Gupta
6 min read
Sponsored
Most applications store what’s true right now: a row that says an order’s status is “shipped,” overwritten in place every time the status changes. Event sourcing stores something different: every change that ever happened, as its own permanent, ordered record, and derives “shipped” by replaying that history. The row you’d normally query becomes a computed view instead of the source of truth. That inversion sounds academic until you hit the specific problem it solves, and expensive once you understand the specific cost it carries.
What actually gets stored
In a conventional design, updating an order’s status means an UPDATE statement that overwrites the previous value. The previous value is gone unless you separately logged it. In event sourcing, nothing gets overwritten. Every change is appended as a new, immutable event:
OrderPlaced { order_id: 501, items: [...], placed_at: "10:03:00" }
PaymentReceived { order_id: 501, amount: 89.00, received_at: "10:04:15" }
ItemBackordered { order_id: 501, sku: "WIDGET-2", eta: "3 days" }
OrderShipped { order_id: 501, carrier: "UPS", shipped_at: "next day, 14:20:00" }
Current state, “this order is shipped, minus one backordered item,” is derived by replaying those events in order, not stored directly. If you need to know what the order looked like at any earlier point, you replay the same events only up to that point. The event log is the only thing that’s durable and authoritative; any “current state” table you build on top is a cache, rebuildable from scratch at any time.
def replay_order(events):
state = {"status": "new", "items": [], "backordered": []}
for event in events:
if event.type == "OrderPlaced":
state["items"] = event.items
state["status"] = "placed"
elif event.type == "PaymentReceived":
state["status"] = "paid"
elif event.type == "ItemBackordered":
state["backordered"].append(event.sku)
elif event.type == "OrderShipped":
state["status"] = "shipped"
return state
What this actually buys you
The obvious benefit is a complete, ordered audit history for free: every change, who or what triggered it, and exactly when, because that’s simply what’s stored. But the less obvious and often more valuable benefit is that new questions about the past become answerable without having anticipated them.
If someone asks six months from now “how many orders had a backorder resolved within 24 hours last quarter,” and you were only ever storing current state, that question is unanswerable unless you happened to log exactly that metric in advance. With the full event history, it’s a new projection: write a new function that walks the same events you already have and computes an answer you never planned for. This is the core structural advantage: the raw material for answering a future question you haven’t thought of yet is already sitting there, because you never threw the history away.
Where it connects to CQRS, and where it doesn’t need to
Event sourcing pairs naturally with CQRS, separating your read and write models, because projecting an event stream into one or more purpose-built read views is a clean way to keep a fast, queryable current-state table in sync with an authoritative event log. That combination, event-sourced writes projected into a denormalized read store, is common enough that people often treat the two patterns as a single package.
They aren’t. You can event-source an entity and simply replay its events on demand when you need current state, no separate read database, no projection pipeline, if the entity isn’t queried often enough or fast enough to need a cached view. And you can run full CQRS with two completely ordinary databases and zero event log, if your write model just needs strict validation and your read model just needs denormalized shape. Reach for event sourcing because you specifically need the history, not because CQRS made the combination sound complete.
The cost that doesn’t show up in the first prototype
The demo version of event sourcing looks clean: append events, replay to get state, done. Production reveals three costs that don’t show up until later.
Schema evolution never stops. An OrderPlaced event you shipped two years ago has whatever shape you gave it then, and you can’t rewrite history to match today’s code. Every event type accumulates versions, and your replay logic needs to keep understanding old shapes indefinitely, not just the current one.
Replay gets slower as history grows. An entity with five years and ten thousand events behind it takes real time to replay from the beginning every time you need its current state. The standard fix is snapshotting, periodically saving computed state at a point in time so replay only needs to process events since the last snapshot, but that’s infrastructure you have to build and keep correct, not something that comes free with the pattern.
Debugging changes shape. “What is this order’s status” stops being “read one row” and becomes “replay N events and check the result.” Good tooling narrows that gap, but it’s a genuinely different mental model for anyone new to the codebase, and it costs onboarding time that a normal table with clear columns doesn’t.
Deciding if you actually need it
| Signal | What it suggests |
|---|---|
| The business has explicitly asked “what did this look like at time X, and why did it change” | Event sourcing is solving a real, named need |
| You’re in a domain where a full audit trail is a compliance requirement (finance, healthcare, regulated inventory) | Worth the operational cost |
| Nobody has ever asked about historical state, only current state | A normal table plus a lightweight audit log covers it |
| Your team has no experience with schema evolution or snapshotting | The learning cost will land before any of the benefit does |
Start by asking whether anyone in your organization actually needs the history, not whether the pattern sounds like better engineering. A ledger, an inventory system, or a workflow under dispute resolution earns event sourcing’s cost easily. A typical CRUD app usually doesn’t, and a normal table with a modest audit log gets you most of the practical benefit without committing to replaying history every time you want to know what’s true right now.
Frequently asked questions
- What is event sourcing, described simply?
- Instead of storing the current state of an entity and overwriting it every time something changes, you store every change as its own immutable event: OrderPlaced, ItemAdded, PaymentReceived, OrderShipped. Current state isn't stored directly; it's computed by replaying all of an entity's events in order. The event log is the source of truth. Current state is a view derived from it, which you can always recompute from scratch.
- How is this different from just keeping an audit log next to a normal table?
- An audit log is usually a side effect: the application still reads and writes a current-state table directly, and the log is a record kept alongside it for compliance or debugging. In event sourcing, there is no current-state table as the source of truth. The events are the only durable, authoritative record; the current-state view, if you keep one, is a cache that gets rebuilt from the events, not the other way around. That inversion is the entire difference, and it's a bigger structural commitment than bolting on a log table.
- Do I need CQRS to do event sourcing, or vice versa?
- No, and treating them as a package deal is a common source of over-engineering. Event sourcing is about how you store state changes. CQRS is about separating your read and write models. You can event-source a write model and still query it directly by replaying events on demand, no separate read store required. You can also run full CQRS with two ordinary databases and no event log at all. They complement each other well, projecting an event stream into a purpose-built read model is a natural fit, which is why they're so often mentioned together, but neither requires the other.
- What's the actual operational cost of event sourcing?
- Three things show up quickly. First, schema evolution: an event shape you defined two years ago has to keep being readable as your code changes, because you can never rewrite history, only add new event types or versioned event schemas. Second, replay performance: an entity with years of events needs snapshotting, periodically saving computed state so you don't replay from event zero every time, or reads get slower every year. Third, a genuinely different debugging habit: 'what is this order's status' becomes 'replay these forty events and check,' which is less immediately legible than reading one row, even with good tooling around it.
- When does event sourcing actually earn its complexity?
- When the history of changes is itself valuable to the business, not just a debugging nicety. Financial ledgers, inventory systems that need to reconstruct exact stock levels at any past point in time, and dispute-resolution workflows where you need to prove exactly what happened and when, are the clearest cases. If nobody in your organization has ever asked 'what did this record look like last Tuesday, and why did it change,' you likely don't need event sourcing yet, and a normal table with a lightweight audit log covers the realistic need at a fraction of the operational cost.
Sponsored
More from this category
More from Web Development
R.01 MySQL Now Has a Native VECTOR Type. Should You Drop pgvector For It?
R.02 React 19.2 vs 'React 20': There Is No React 20, and Here's What's Actually New
R.03 Next.js's First Scheduled Security Release Shipped: 9 Advisories, What to Patch
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored