~/software-design/event-sourcing-pattern-explained.md

Event Sourcing, Explained: Store What Happened, Not Just the Final State

4 min readNovember 03, 2026by Gorav Singal#event-sourcing#cqrs#system-design
// TL;DR

In normal CRUD you store the current state and overwrite it on every change — the moment you UPDATE a row, the history is gone. Event Sourcing flips this: you store the sequence of FACTS that happened (AccountOpened, Deposited, Withdrew) as an append-only, immutable log, and the current state is simply a left-fold over those events (balance = 0 + 5000 − 2000 + 1000 = ₹4,000). This gives you a perfect audit trail for free, time-travel (rebuild state as of any date), and the ability to create brand-new read models from old events later. The costs: a real mindset shift, replay cost (fixed with periodic SNAPSHOTS), and the discipline to VERSION events carefully because they are immutable and live forever. It pairs naturally with CQRS — the events are your write side, and projections turn them into whatever read models the UI needs. Great for ledgers, orders, and anything needing a strong audit trail; overkill for simple apps.

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a balance of ₹4,000. Can you tell me why it is ₹4,000? With a normal database, the honest answer is “no idea” — the table just has balance = 4000, and whatever happened before has been overwritten into oblivion. Event Sourcing is the pattern that lets you answer that question properly, and it is simpler than it sounds.

The one big idea

In a normal (CRUD) system we store the current state and keep overwriting it. Every deposit does an UPDATE accounts SET balance = .... The new value replaces the old, and the old is gone forever.

Event Sourcing says: don’t store the state — store the events that led to it. Keep an append-only, immutable log of everything that happened, and compute the current state by replaying that log whenever you need it.

Event Sourcing architecture: an append-only immutable event log holds the sequence AccountOpened, Deposited 5000, Withdrew 2000, Deposited 1000. The current state is computed by folding or replaying the events — balance equals 0 plus 5000 minus 2000 plus 1000 equals 4000. From the same events you build projections and read models, keep snapshots to save state every N events so replay stays fast, and get audit and time-travel for free with full history and the ability to rebuild state at any point. The mindset shift: in CRUD you store current state and overwrite it, losing history on UPDATE, whereas event sourcing stores the facts and derives state as a left-fold over them.

So for the bank account, instead of one row that says 4000, we store:

#1  AccountOpened
#2  Deposited  ₹5,000
#3  Withdrew   ₹2,000
#4  Deposited  ₹1,000

And the balance is just a fold over these facts: 0 + 5000 − 2000 + 1000 = ₹4,000. Now the “why is it 4,000?” question answers itself — the full story is right there.

Why this is so powerful

Once you store facts instead of state, a lot of hard things suddenly become easy:

  • Audit trail for free. Who did what, and when — it is literally your data model, not a separate logging system you bolted on and hope is correct.
  • Time-travel. Want the balance as of last Tuesday? Replay events up to that timestamp. This is genuinly magical the first time you use it for debugging a production issue.
  • New read models from old data. Six months later, product wants a new dashboard. With CRUD you only have today’s state. With events, you replay history and build the new view as if it always existed.
  • Debugging. You can reconstruct the exact state the system was in when a bug happened, which is otherwise near impossible.

CRUD overwrites, Event Sourcing remembers

CRUD versus Event Sourcing: traditional CRUD stores state in an accounts table with one row, id 42 balance 4000, and every deposit or withdraw UPDATEs the row so previous values are gone and nobody can say what happened. Event Sourcing stores facts in an append-only events table — opened, plus 5000, minus 2000, plus 1000 — where nothing is overwritten, the balance is derived, and you can answer who did what and when, replay to any date and add new views for free. It pairs beautifully with CQRS: the events are the write side and projections turn them into read models, which is how banks, ledgers and order systems are built, provided you use snapshots and version your events carefully.

It pairs beautifully with CQRS

This is where it clicks. Event Sourcing and CQRS are different patterns, but they fit together perfectly. The events are your write side — the one true source of truth. Then projections (which are just functions that consume events) build whatever read models the UI needs — a balance summary, a transaction list, a monthly statement — each in its own shape. One stream of facts in, many tailored views out. This is exactly how ledgers, banking systems, order-management and anything with a serious audit requirement are built.

A tiny sketch:

# an event is just an immutable fact
events = [AccountOpened(), Deposited(5000), Withdrew(2000), Deposited(1000)]

# current state = fold over the events
def balance(events):
    bal = 0
    for e in events:
        if isinstance(e, Deposited):  bal += e.amount
        elif isinstance(e, Withdrew): bal -= e.amount
    return bal

balance(events)          # ₹4,000  — always derived, never stored directly

The two things you must get right

Event Sourcing is lovely, but it is not free. Two things will hurt you if you ignore them.

  1. Snapshots. If an account has a million events, you do not want to replay all of them every single time just to show a balance. The fix is simple: every N events, save a snapshot of the state, and replay only from the last snapshot forward. Keep this in mind from day one, beacuse retro-fitting it is painful.

  2. Event versioning. Events are immutable and they live forever. The Deposited event you wrote today will still be read in five years. So when your schema changes (you add a currency field, say), you must handle old events gracefully — through upcasting (transforming old events to the new shape on read) or versioned event types. An old event schema is a promise, and you have to keep it.

Also — Event Sourcing is genuinely overkill for a simple app. If you don’t need audit, history or time-travel, a normal table is absolutely the right call. Don’t event-source your to-do list.

Takeaways

  • Store the events (facts that happened), not the current state. Current state is a fold/replay over the events.
  • You get a perfect audit trail, time-travel, and the ability to build new read models from old data — all for free.
  • Keep it fast with snapshots; keep it safe over time by versioning events carefully (they are immutable and forever).
  • It pairs naturally with CQRS — events are the write side, projections build the read models.
  • Use it for ledgers, orders and audit-heavy domains; skip it for simple CRUD.

Part of the event-driven data patterns series: CQRS (its partner), Change Data Capture, the Outbox pattern, and the idempotency article. For the full picture, the System Design Masterclass and Database Internals courses go deeper.

Share

Related Posts

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

Most of us start our career with one model for everything. The same class, the…

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

The Circuit Breaker Pattern: Fail Fast, So One Dead Service Doesn't Kill Them All

The Circuit Breaker Pattern: Fail Fast, So One Dead Service Doesn't Kill Them All

Like the electrical switch in your home that it is named after, a circuit…

Change Data Capture (CDC): Stream Every Database Change Without Touching Your App

Change Data Capture (CDC): Stream Every Database Change Without Touching Your App

Your orders live in Postgres. But you also need them in Elasticsearch for search…

Message Queue vs Stream — RabbitMQ/SQS vs Kafka, and When to Use Which

Message Queue vs Stream — RabbitMQ/SQS vs Kafka, and When to Use Which

“Should we use RabbitMQ or Kafka?” is one of the most common — and most muddled…

Idempotency in Distributed Systems — Why Your Retry Just Charged the Customer Twice

Idempotency in Distributed Systems — Why Your Retry Just Charged the Customer Twice

Here is a bug that has cost real companies real money: a customer taps “Pay ₹50…

Latest Posts

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

In the previous article I argued that AI is not killing software engineering…

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

A few months back I approved a PR that was almost entirely AI-generated. It…

How to Debug AI-Generated Code: Understand First, Fix Last

How to Debug AI-Generated Code: Understand First, Fix Last

In the last article I wrote about reviewing AI code before it goes in. This one…

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

Every few weeks somebody sends me a scary headline — “AI will replace all…

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

Most of us start our career with one model for everything. The same class, the…