Cloud · Messaging · DWG NO. 041

Event Sourcing

Store every change as an event in an append-only log, and derive current state by replaying them, instead of only keeping the latest value.

TypeMessaging / Data
ScopeData storage model
ComplexityHigh
Common inFinancial ledgers, audit-heavy systems, event-driven architectures

Overwriting a row throws away how you got there

A typical table update replaces the old value with the new one — the account balance goes from $80 to $100, and the fact that it happened through a $20 deposit is gone. Reconstructing "what happened and when" for an audit, a dispute, or a bug investigation is impossible once the history has been overwritten.

The event log is the source of truth

Instead of storing current state directly, every change is appended to an event log as an immutable fact (`DepositMade`, `WithdrawalMade`). Current state is derived by replaying all events for an entity in order — often cached as a snapshot so a full replay isn't needed on every read.

Opened$0Deposited $80Withdrew $20Deposited $40replay → balance = $100Current state

An account balance rebuilt from an event log

`getBalance` never reads a stored balance field — it recomputes it by folding over every event that ever happened to the account.

event-sourced-account.js
const eventLog = [];

function append(accountId, event) {
  eventLog.push({ accountId, ...event, at: eventLog.length });
}

function getBalance(accountId) {
  return eventLog
    .filter((e) => e.accountId === accountId)
    .reduce((balance, e) => {
      if (e.type === "Deposited") return balance + e.amount;
      if (e.type === "Withdrawn") return balance - e.amount;
      return balance;
    }, 0);
}

append("acct-1", { type: "Deposited", amount: 80 });
append("acct-1", { type: "Withdrawn", amount: 20 });
append("acct-1", { type: "Deposited", amount: 40 });

console.log(getBalance("acct-1")); // 100 — derived, not stored directly
console.log(eventLog.filter((e) => e.accountId === "acct-1")); // full history, still intact

Trade-offs