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.
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,000And 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
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 directlyThe two things you must get right
Event Sourcing is lovely, but it is not free. Two things will hurt you if you ignore them.
-
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.
-
Event versioning. Events are immutable and they live forever. The
Depositedevent you wrote today will still be read in five years. So when your schema changes (you add acurrencyfield, 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.










