Concepts / Storage and data / Double-entry ledgers

Double-entry ledgers

Recording money as paired entries so totals always balance and history is immutable.

01The intuition

The problem it solves

A shopkeeper keeps a single column of balances: Rao owes 500, Sharma owes 200. When Rao pays 300, the shopkeeper changes 500 to 200 and moves on. A week later Rao says "I paid 300", the shopkeeper says "the book says 200", and neither can prove how it got there. The balance is right, perhaps, but the history is gone and an error cannot be found because there is nothing to add up.

Money in a system is the same, with a million Raos and a program that can crash halfway through an update. Double-entry bookkeeping, five centuries old, is the fix: never change a balance, only append entries, and write every movement twice, once where it leaves and once where it arrives, so the books always add up to zero and any error shows as an imbalance.

Build it one piece at a time

Step 1: accounts and entries. Every party gets an account, including the company's own (cash, fees, pending). A transfer is not an update; it is two entries with the same transaction id, one debit and one credit, appended to an immutable table.

txnaccountdebitcredittime
t1rao30010:00
t1shop30010:00

Step 2: balances are sums. Rao's balance is the sum of his credits minus his debits over all entries. Nothing is ever edited; a mistake is corrected by a new reversing transaction. The history is the data.

Step 3: the invariant. Within a transaction, debits equal credits. Across the whole ledger, the sum of all balances is therefore zero (money moved, never created). A nightly job checks this; if it is not zero, something was written once instead of twice, and the broken transaction id is found by checking each.

Step 4: write both entries atomically. The two entries must appear together or not at all. On one database that is a transaction. Across shards (Rao and the shop on different machines) it needs a distributed transaction or a saga with a pending state: write "t1 pending", apply each side, mark "t1 posted", and a reconciler finishes or reverses anything left pending.

Step 5: make repeats harmless and reads fast. The client's transaction id is the idempotency key: a retried transfer with the same id is a no-op, never a second payment. Summing a million entries per balance read is slow, so keep a running balance per account updated in the same transaction, or a periodic snapshot plus the entries since, and treat the snapshot as a cache that the entries can always rebuild.

Why it works, with the numbers

Append-only entries give an audit trail for free and make correctness checkable by addition:

for every transaction t: Σ debits(t) = Σ credits(t); hence over all accounts Σ balances = 0

Storage grows with activity: 100 million transfers a day at two entries of 100 bytes is 20 GB a day, 7 TB a year, cheap for what it buys. A balance read from a snapshot plus today's entries touches a handful of rows. The guarantee is that money is conserved and every rupee can be traced. The failure modes are a crash between the two entries without a pending state (the nightly check finds it, but a customer may see a wrong balance meanwhile), a retried transfer without an idempotency key (double payment, the classic), and floating-point money (use integers in the smallest unit, paise or cents).

The trade-off you must say out loud

A ledger is more rows, more care and a reconciliation job, in exchange for correctness you can prove. Say that balances are derived, that entries are immutable, and how a transfer across shards is made atomic. The interviewer's follow-up is usually "what if the external bank confirms late or never?", and the answer is the pending state with a timeout and a compensating entry.

Where it shows up in the 100 Google problems

02Used in these problems