Quick Answer

Event sourcing stores every state change as an immutable event in an append-only log, instead of storing just the current state. Current state is derived by replaying all events in order through a pure function. Appending a new event never rewrites history, so you can always recompute state as of any point in time by replaying a prefix of the log. The demo below appends bank-account events, derives a balance from all of them, then appends one more and recomputes without touching anything already stored.

Storing State vs. Storing What Happened

The conventional approach stores current state directly: an accounts table with a balance column, updated in place every time money moves. Event sourcing stores something different, every deposit and withdrawal as its own immutable record, appended to a log that's never edited or deleted. Current balance isn't stored anywhere; it's calculated.

That sounds like more work for a simpler question, and for a single balance lookup, it is. What it buys back: the full history of how you got there is never thrown away, because it was never overwritten in the first place. Every past state is reconstructible, not just the current one.

The demo below appends real events to an in-memory array and derives the balance by replaying them, actual executed code with actual printed output, not pseudocode.

The same idea applies well beyond bank balances — shopping cart contents, a document's edit history, or a game character's stats can all be modeled the same way, as a sequence of changes rather than a single mutable snapshot that overwrites what came before it.

Deriving State by Replay

function deriveState(eventLog) {
  let state = { balance: 0, history: [] };
  for (const event of eventLog) {
    switch (event.type) {
      case 'AccountOpened':
        state = { balance: event.payload.openingBalance, history: [...state.history, 'opened'] };
        break;
      case 'FundsDeposited':
        state = { balance: state.balance + event.payload.amount, history: [...state.history, 'deposit'] };
        break;
      case 'FundsWithdrawn':
        state = { balance: state.balance - event.payload.amount, history: [...state.history, 'withdraw'] };
        break;
    }
  }
  return state;
}

After appending AccountOpened(1000), FundsDeposited(500), and FundsWithdrawn(200), calling deriveState(events) on the real, executed script gives:

{ balance: 1300, history: [ 'opened', 'deposit 500', 'withdraw 200' ] }

1000 + 500 − 200 = 1300, computed by replaying the log, not by reading a stored total anywhere. Each case in the switch returns a brand-new state object rather than mutating the previous one in place, which matters: since deriveState is a pure function of the event log, calling it twice on the same events always produces the same result, with no hidden state carried between calls.

Adding a New Event Without Touching History

Append one more event, FundsDeposited(300), and call deriveState again on the same array:

[APPEND] FundsDeposited {"amount":300}

{ balance: 1600,
  history: [ 'opened', 'deposit 500', 'withdraw 200', 'deposit 300' ] }

1300 + 300 = 1600, and critically, the first three events were never touched to get there, the log grew from 3 entries to 4, and deriveState simply replayed all of them again from the start. Checking it directly after the run confirms it: events.length is 4, and the first event still matches { type: 'AccountOpened', payload: { openingBalance: 1000 } } exactly.

This is the core discipline event sourcing enforces: you never go back and edit a past record to reflect new information. You only ever append. If a deposit was recorded in error, you fix it by appending a compensating event, not by rewriting history. That discipline is what makes the log trustworthy as the single source of truth — nothing downstream ever has to wonder whether a past record was quietly changed after the fact.

The Free Feature: Time Travel

Because state is derived, not stored, asking "what was the balance after just the first two events" is a matter of replaying a shorter slice of the same log, no separate history table, no snapshot taken in advance:

const pastState = deriveState(events.slice(0, 2));
// { balance: 1500, history: [ 'opened', 'deposit 500' ] }

1000 + 500 = 1500, the balance exactly as it stood before the withdrawal and the second deposit ever happened, computed from data that was already there. A system storing only current balance has no way to answer that question after the fact unless it separately logged every change, which is precisely what event sourcing makes the primary record instead of an afterthought.

This is genuinely useful outside of curiosity: reconstructing what a customer's account looked like at a specific moment is a common support and audit request, and with event sourcing it costs nothing extra to answer, because the data needed to reconstruct it was never thrown away in the first place.

The Real Costs

Replaying every event from the beginning gets slow as the log grows, an account with ten years of transactions shouldn't replay ten years of events on every balance check. Production systems solve this with snapshots: periodically save the derived state, and on the next read, replay only the events appended after that snapshot.

There's also a schema problem: events already in the log can never be edited, so if FundsDeposited's shape needs to change five years from now, old events still have the old shape. Systems handle this with "upcasting", a small function that transforms an old event's shape into the new one during replay, so the reducer only ever has to understand the current format.

And a query problem: an append-only log is excellent for writes and terrible for "show me all accounts over ₹50,000", which is why event sourcing is so often paired with CQRS, projecting the log into a queryable read model instead of scanning it directly.

Frequently Asked Questions

How is this different from just keeping an audit log? An audit log usually sits next to the 'real' stored state as a side record. In event sourcing there is no other stored state, the events are the only source of truth, and current state is always derived from them.
Doesn't replaying every event get slow over time? Yes, for long-lived logs. Production systems take periodic snapshots of the derived state and replay only the events appended after the most recent snapshot, instead of the entire history.
Can I fix a mistake in a past event? No, the log is append-only and immutable by design. You correct a mistake by appending a new compensating event, such as a reversal, never by editing or deleting the original.
Does event sourcing require CQRS? No, but they pair naturally. The event log is optimized for writes, so most real systems also build a separate read-optimized projection from it rather than replaying the full log for every query.
What happens when an event's data shape needs to change? You need a versioning strategy, usually called upcasting, that transforms old event shapes into the current one during replay, since events already written can never be edited in place.