AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Event Sourcing: Building Audit Trails

📚 Architecture⏱️ 24 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open the UPI app on your phone and check your bank balance. It shows a single number — say ₹370. But scroll down, and there is a full list underneath it: every credit and debit, with dates, amounts, and who it was to or from. Here is the question this chapter is really about: why does the app bother keeping that whole list at all? If all it needs to show you is ₹370, why not just store the number ₹370 somewhere and update it every time money moves, the way a simple counter works?

The answer is that the list is the real data, and the number ₹370 is something the app calculates from the list, every single time you open the screen. This idea — store the sequence of things that happened, and calculate the current state from that sequence instead of storing the current state directly — is called event sourcing. It is one of the most important ideas behind systems that need trustworthy history: bank passbooks, IRCTC ticket status, GST invoices, and the version history of every file in a Git repository. By the end of this chapter you will be able to write the two small functions that make event sourcing work, trace them by hand on real numbers, and explain precisely why a system built this way can answer questions that a system storing only "the current value" cannot.

The naive approach: store the balance, overwrite it

Let's build Meera's savings account the way a beginner programmer most naturally would. She opens the account with zero rupees. Four things happen to it over one week in August 2026:

  • Aug 1 — salary credit, DEPOSIT ₹500
  • Aug 3 — electricity bill, WITHDRAW ₹200
  • Aug 5 — refund from an online return, DEPOSIT ₹150
  • Aug 7 — grocery shopping, WITHDRAW ₹80

The obvious way to store this in a database is one table with one row per account, holding the current balance, and one UPDATE statement every time something happens:

CREATE TABLE account (
  id      INT PRIMARY KEY,
  balance INT
);

INSERT INTO account VALUES (1, 0);

UPDATE account SET balance = balance + 500 WHERE id = 1;  -- Aug 1
UPDATE account SET balance = balance - 200 WHERE id = 1;  -- Aug 3
UPDATE account SET balance = balance + 150 WHERE id = 1;  -- Aug 5
UPDATE account SET balance = balance -  80 WHERE id = 1;  -- Aug 7

Run these four statements in order and the account table ends up holding exactly one row: id = 1, balance = 370. That is correct — it is Meera's real balance. This is called CRUD style storage (Create, Read, Update, Delete), because the defining move is the UPDATE: each new fact overwrites the previous value in place. It's simple, it's fast to read, and it is exactly how most beginner database exercises are taught. For a huge number of applications, it's the right choice.

But now ask a question a bank legally has to be able to answer: what was Meera's balance on the morning of August 4th, before the Aug 5 refund landed? Look at the account table above. It has one row. It has always had one row. Every UPDATE replaced the previous number with a new one, and the previous number is gone — not archived, not hidden somewhere, permanently unrecoverable from this table. The database never lied to you at any single moment, but it has no memory. You cannot rewind it, because rewinding was never designed in. This is the core weakness of CRUD storage: it tells you the current state of the world, and nothing about how the world got there.

Storing what happened, instead of what is true right now

Event sourcing flips the design. Instead of a table that holds the *current* balance and gets overwritten, you keep a table that only ever grows — a table you append to and never update or delete from. Each row is an event: an immutable, timestamped record of one thing that actually happened.

CREATE TABLE account_events (
  id          INT PRIMARY KEY,
  account_id  INT,
  event_date  DATE,
  event_type  TEXT,   -- 'DEPOSIT' or 'WITHDRAW'
  amount      INT
);

INSERT INTO account_events VALUES (1, 1, '2026-08-01', 'DEPOSIT',  500);
INSERT INTO account_events VALUES (2, 1, '2026-08-03', 'WITHDRAW', 200);
INSERT INTO account_events VALUES (3, 1, '2026-08-05', 'DEPOSIT',  150);
INSERT INTO account_events VALUES (4, 1, '2026-08-07', 'WITHDRAW',  80);

Notice there is not a single UPDATE or DELETE anywhere in that block — only INSERT. Nobody ever touches rows 1 through 3 again. There is no column called balance in this table at all, because the table's job is not to hold the balance. Its job is to hold facts. The balance is something we are going to compute whenever we need it, by walking through the facts in order and adding them up. That computation is called replaying the events, and the function that does it is the single most important piece of code in this chapter.

function replay(events) {
  let balance = 0;
  for (const e of events) {
    if (e.type === 'DEPOSIT') {
      balance += e.amount;
    } else if (e.type === 'WITHDRAW') {
      balance -= e.amount;
    }
  }
  return balance;
}

Let's trace it by hand, exactly the way you would on a CBSE practical exam, because tracing code line by line is the skill this chapter is really testing. We call replay with the four events, in date order, and balance starts at 0.

  • Event 1 (Aug 1, DEPOSIT 500): balance = 0 + 500 = 500
  • Event 2 (Aug 3, WITHDRAW 200): balance = 500 - 200 = 300
  • Event 3 (Aug 5, DEPOSIT 150): balance = 300 + 150 = 450
  • Event 4 (Aug 7, WITHDRAW 80): balance = 450 - 80 = 370

The loop ends because there are no more events, and replay returns 370. That matches the ₹370 balance Meera sees in her app. Two systems, two completely different storage strategies, and they agree on the number that matters — which is exactly what you'd want. The difference only shows up when you ask a question about the past, not the present.

Why the diagram in your head should be "append, then fold" — not "store and edit"

The picture below puts the two designs side by side, using the same four events for both, so you can see precisely what each one keeps and what each one throws away.

Same four events, two ways to store Meera's balance Aug 1 salary deposit → Aug 7 grocery withdrawal CRUD: overwrite in place Event sourcing: append, then replay Aug 1 · UPDATE balance = 0 + 500 ₹500 Aug 3 · UPDATE balance = 500 − 200 ₹300 Aug 5 · UPDATE balance = 300 + 150 ₹450 Aug 7 · UPDATE balance = 450 − 80 ₹370 Only ONE row survives in the table: balance = ₹370 ✗ The ₹500 / ₹300 / ₹450 values are gone forever. Can't answer: "what was the balance on Aug 4?" Aug 1 · DEPOSIT · +₹500 Aug 3 · WITHDRAW · −₹200 Aug 5 · DEPOSIT · +₹150 Aug 7 · WITHDRAW · −₹80 replay: fold all 4 events in order Current balance (derived, not stored) ₹370 ✓ All 4 raw facts are kept forever. Replay only the events up to any past date to get the balance as it was on that date. Both panels start from the SAME four events and reach the SAME ₹370 — the difference is what survives afterward. Delete the balance column on the left, and Meera's history is gone. Delete the "current balance" cache on the right, and nothing is lost — just replay the events again and ₹370 comes back.

Read the two panels as one experiment run twice. On the left, each UPDATE replaces the single cell that holds the balance — by the time you reach Aug 7, the values ₹500, ₹300, and ₹450 have already been overwritten and cannot be recovered from that table by any query, no matter how clever. On the right, nothing is ever overwritten. The four facts sit in the log exactly as they were written on the day they happened, and the balance ₹370 at the bottom is not a stored number at all — it is the output of running replay over the log at the moment you asked. If you deleted that green result box, it would cost you nothing: run replay(events) again and ₹370 reappears, because the log still has everything it needs. If you deleted the log on the left, the account's entire history is gone, even though the number 370 might still be sitting there.

Asking about the past: state at a point in time

Storing raw events unlocks a kind of query CRUD simply cannot answer: the balance as of an arbitrary past date. This is precisely what a bank statement, an IRCTC PNR status history, or a Git file history all let you do — reconstruct what was true at a moment before the present. Here is the function, built as a small variation on replay:

function balanceAsOf(events, cutoffDate) {
  let balance = 0;
  for (const e of events) {
    if (e.date > cutoffDate) break;   // event happened after cutoff — stop
    balance += (e.type === 'DEPOSIT') ? e.amount : -e.amount;
  }
  return balance;
}

This works because the events are stored in date order, and because the dates are written as ISO-format text ('2026-08-05', year first, then month, then day). Two dates written this way can be compared with an ordinary > the same way you compare two words alphabetically, and the comparison will always match calendar order — '2026-08-05' > '2026-08-04' is true, exactly like it should be. This is a real, deliberate reason database designers store dates as YYYY-MM-DD rather than DD-MM-YYYY: the second format compares wrong ("05-08-2026" looks alphabetically smaller than "31-01-2026" even though it's a later date), while the first format sorts correctly for free.

Let's trace balanceAsOf(events, '2026-08-04') — asking for the balance on August 4th, the day between the electricity bill and the refund.

  • Event 1, date '2026-08-01': is '2026-08-01' > '2026-08-04'? No. Add: balance = 0 + 500 = 500.
  • Event 2, date '2026-08-03': is '2026-08-03' > '2026-08-04'? No. Add: balance = 500 - 200 = 300.
  • Event 3, date '2026-08-05': is '2026-08-05' > '2026-08-04'? Yes. The loop breaks immediately — event 3 and event 4 are never looked at.

The function returns 300. That's Meera's real balance on the morning of Aug 4th — after the electricity bill went out, before the refund landed. No CRUD table that only ever stores "the current balance" can produce this number once August has passed, because by the time you ask, the row has already been overwritten twice more. The event log doesn't just happen to contain this answer — it was designed specifically so that every past state is always one small computation away.

Two misconceptions worth correcting directly

Misconception 1: "Event sourcing just means keeping a log table next to your normal database, for backup." This describes something real — an audit log — but it is not event sourcing. The distinguishing feature of event sourcing is that the event log is the only source of truth. There is no separate "real" balance column that the log merely shadows. If a system keeps a balance column that gets updated directly and a log on the side documenting what happened, and the two ever disagree, an audit log cannot tell you which one is correct — they're independent records that might drift apart. In true event sourcing there is nothing to drift: any "current balance" you see is calculated fresh from the log, so it is mathematically impossible for it to disagree with the log. It isn't a second copy of the truth; it's the only copy, computed on demand.

Misconception 2: "Since you replay every event from the start, this must get unbearably slow once an account has millions of transactions." This is a fair worry, and real systems solve it with snapshots — but a snapshot is only ever a performance shortcut, never a second source of truth. Suppose Meera's account accumulates ten thousand events over three years. A system can periodically compute replay(events) up to, say, 1 January 2026, store that single number as a snapshot labeled "balance as of 2026-01-01: ₹12,400", and then to answer "what is the balance today?" it only needs to replay events after 1 January 2026 and add them to the snapshot — not all ten thousand from the beginning. The crucial test for whether something is a legitimate snapshot: if you deleted it, could you rebuild it exactly by replaying the raw events again? If yes, it's a cache, and the design is still honestly event-sourced. If deleting it would lose information forever, it was secretly acting as the real source of truth all along, and the system has quietly turned back into CRUD wearing a disguise.

Where this shows up outside a savings account

Event sourcing is not a niche banking trick — once you know the pattern, you'll recognise it everywhere data needs to be trustworthy after the fact.

Git. Every commit you make is an immutable event: "these exact lines changed, at this time, by this author." Git never edits an old commit to reflect a later change — it appends a new commit on top. The content of a file that you see in your editor right now is not stored anywhere as a single "current version" object; it is derived by replaying the chain of commits from the very first one. That is exactly the replay function from this chapter, just operating on line-changes instead of rupee amounts.

IRCTC PNR status. When you book a waitlisted train ticket, the status doesn't just silently change from "WL 24" to "WL 9" to "CNF" with the earlier numbers erased. The system holds a record of each status-changing event, which is why the tracking page can show you the whole progression of your waitlist position over the days before the chart was prepared, not merely today's status.

GST credit notes. If a business needs to correct an invoice it already issued — say, the customer returned goods — Indian tax law does not let it go back and edit the original invoice. It must issue a new document, a credit note, which is itself a fresh, independently dated event referencing the original. The original invoice and the correction both remain on record forever; nothing is overwritten, because tax authorities need to be able to audit exactly what happened and when, not just see a final adjusted number.

Notice the pattern common to all three: whenever the true requirement is "I need to be able to prove what happened, not just state what is true right now," the design converges on the same idea — never overwrite, only append, and compute whatever "current state" means by folding the history. That single sentence is worth memorising, because it is the test you can apply to decide whether a system you design should use CRUD or event sourcing: if losing the ability to reconstruct the past would be a serious problem, event sourcing earns its extra complexity; if only the present matters, plain CRUD is simpler and correct.

Connecting this to what you already know

You have almost certainly written the CRUD version of this pattern already, even without naming it. Any Python program with a single variable that you keep reassigning — balance = balance + 500, then later balance = balance - 200 — behaves exactly like the UPDATE statements on the left side of the diagram: each new assignment destroys the previous value, and by the end of the program only the final number survives, with no way to ask what the variable held three lines ago. Event sourcing is what you get when, instead of reassigning one variable, you append every change to a list, and write a function — a loop, exactly like replay — that walks the list and computes whatever summary you currently need. The list is the memory. The function is how you read it. This is the same relationship between "storing data" and "a function that processes a list" that you already use for far simpler tasks, like summing marks in a list of test scores — replay is just that same loop, applied to a list of things that happened to a bank account instead of a list of numbers.

Practice: trace it yourself

Q1. A shop's petty-cash tin is tracked with these events, in order: DEPOSIT 200, WITHDRAW 60, WITHDRAW 45, DEPOSIT 100. Trace replay(events) by hand, writing the balance after each event, and give the final answer.

Q2. A student's exam-prep tracker logs practice-test scores as events: DEPOSIT 10, DEPOSIT 25, WITHDRAW 5, DEPOSIT 40 (a "withdraw" here represents marks deducted for negative marking). Trace the running total after each event.

Q3. Using Meera's original four events (Aug 1 DEPOSIT 500, Aug 3 WITHDRAW 200, Aug 5 DEPOSIT 150, Aug 7 WITHDRAW 80), what does balanceAsOf(events, '2026-08-06') return? Trace the loop and show which event causes it to break.

Q4. A classmate says: "I already do event sourcing — my app has a balance column, and I also keep a transaction_log table that records every change, just in case." Explain, in your own words, what is missing from this understanding, using the "delete it and see what survives" test from this chapter.

Q5. Explain in one or two sentences why storing dates as '2026-08-05' (year-month-day) rather than '05-08-2026' (day-month-year) matters for a function like balanceAsOf that compares dates with >.

Answer key

A1. Start at 0. After DEPOSIT 200: 200. After WITHDRAW 60: 140. After WITHDRAW 45: 95. After DEPOSIT 100: 195. Final balance: 195.

A2. Start at 0. After DEPOSIT 10: 10. After DEPOSIT 25: 35. After WITHDRAW 5: 30. After DEPOSIT 40: 70. Running totals: 10, 35, 30, 70.

A3. Checking each event's date against the cutoff '2026-08-06': Aug 1 is not greater than Aug 6, add 500 → 500. Aug 3 is not greater, subtract 200 → 300. Aug 5 is not greater, add 150 → 450. Aug 7 is greater than Aug 6, so the loop breaks before touching it. The function returns 450 — the balance right after the Aug 5 refund, before the Aug 7 grocery withdrawal.

A4. Apply the "delete it and see what survives" test to each piece: delete the transaction_log table — does the app still work? Yes, because the balance column is what the app actually reads from and updates. That means balance, not the log, is the real source of truth, and the log is just a side note that could silently fall out of sync with it (for example, if someone updates balance directly through a database console without also inserting a log row). True event sourcing requires the reverse: delete the balance column, and the app should be completely unaffected, because it never actually depended on that column — it always recomputes the balance from the log.

A5. YYYY-MM-DD dates compare correctly with an ordinary text comparison because the most significant part of the date — the year — always comes first, then month, then day, so '2026-08-01' sorts textually before '2026-08-05', exactly matching calendar order. With DD-MM-YYYY, the day comes first, so a text comparison would incorrectly treat '05-01-2027' as smaller than '31-12-2026', even though the second date is chronologically earlier — breaking the > check that balanceAsOf depends on.

Summary

A CRUD-style table stores the current state of the world and destroys the past every time an UPDATE runs — fast to read, but structurally unable to answer "what was true before?" once the data has changed again. Event sourcing stores the sequence of immutable, append-only facts that occurred — events — and computes any state you need, including the current one, by replaying those events with a function that starts from a known baseline and folds each event into it in order. replay(events) gives you the present; giving that same fold a cutoff and a break condition, as in balanceAsOf(events, date), gives you any moment in the past, because nothing was ever thrown away. Snapshots make replaying large histories fast without giving up this guarantee, as long as a snapshot is something you could always regenerate by replaying from scratch. The pattern — append, never overwrite; derive the present by folding the past — is the same one running underneath Git commits, IRCTC PNR status history, and GST credit notes, and it is worth recognising by name the next time you design a system that has to prove, not just state, what happened.

Think About It

Think about this: How would you explain event sourcing: building audit trails to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

← Database Sharding: Horizontal ScalingWebSocket Advanced: Building Real-time Systems →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn