Thoughts
Double-entry is 500 years old and still the right data model
If you’ve never built a financial system, the obvious design for balances looks like this: a
balance column on the account row, and you add or subtract as money moves. It’s simple, it’s
fast, and it is the thing that will eventually betray you. I want to walk through why, and why
the technique Venetian merchants wrote down in the 1400s is still the answer.
The mutable balance trap
A single mutable number has no memory. When a balance is wrong — and someday it will be — the number tells you what it is, never how it got there. You can’t answer “why is this account at 4,200?” because you threw away every step that led there. You kept the destination and burned the map.
It gets worse under concurrency. Two transfers hitting the same account race on read-modify-write, and without careful locking you get lost updates: money that quietly evaporates or duplicates. You can paper over it with row locks, but you’re now serializing on the hottest rows in the system to protect a design that was fragile to begin with.
What double-entry actually is
Double-entry reframes the whole thing. You don’t store balances; you store entries, and every movement of money is recorded twice — a debit on one account and a matching credit on another. The rule that makes it work: for every transaction, debits and credits sum to zero. Money is never created or destroyed, only moved from one account to another.
The balance stops being a stored fact and becomes a derived one: the sum of an account’s entries. And the entries are append-only — you never edit or delete them. Made a mistake? You post a correcting entry. The history stays intact.
Two properties fall out of this for free, and they’re the two that matter most in finance:
- It’s auditable by construction. Every number can be traced to the entries that produced it. When an auditor (or an angry customer) asks you to explain a balance, the answer is right there in the log.
- It has a built-in integrity check. Because every transaction must net to zero, the whole system must always net to zero. If it doesn’t, you have a bug — and you know immediately, instead of discovering it a quarter later.
The trade-off, honestly
This isn’t free. Append-only means the entries table grows without bound, and deriving a balance by summing millions of rows on every read is a non-starter. So real ledgers keep balance snapshots — a cached running balance at a point in time — and sum only the entries since the last snapshot. You get the audit trail and fast reads, at the cost of a caching layer you have to keep honest.
That’s the actual engineering: not “should I use double-entry” (you should), but where do the snapshots live, how often do you cut them, and how do you prove they still match the entries. Square wrote up a nice real-world version of this — an immutable, append-only ledger on top of Spanner — here.
The lesson I keep relearning: when the data model has correctness baked into its shape, you spend your energy on performance, which is a tractable problem. When you optimize first and bolt on correctness later, you spend your energy on incidents. The accountants picked the first path 500 years ago. It still holds.
Archie