Never Edit the Past
Building an accounting core showed me that bookkeepers solved immutable history, derived state and integrity checks five centuries before software had words for them.
- Published
- Reading time
- 4 min read
- Length
- 708 words
- Topics
- Accounting, Data modeling, SaaS
From the outside, accounting looks like arithmetic with strict rules. From inside the accounting core of RentAxis, where I built the chart of accounts, journal entries and bank reconciliation, it looks like something else: one of the oldest data models still in production, and one that solved problems software keeps rediscovering.
Luca Pacioli described double-entry bookkeeping in 1494, in Summa de arithmetica, as the way Venetian merchants already kept their books. Five centuries later it still answers questions about immutable history, derived state and data integrity more cleanly than most systems built this decade.
Every Transaction Has Two Sides
The core rule is simple. Every transaction is a journal entry with at least two lines, and the debits must equal the credits. When a tenant pays $1,200 of rent, cash goes up and the tenant’s receivable goes down by the same amount. Money never appears or disappears. It moves between accounts, and the entry records both ends of the move.
That rule is an invariant, enforced on every write. In code it fits in a few lines:
type Line = { account: string; debit: number; credit: number }; // integer cents
type Entry = { id: string; date: string; memo: string; lines: Line[] };
function assertBalanced(entry: Entry): void {
const debits = entry.lines.reduce((sum, line) => sum + line.debit, 0);
const credits = entry.lines.reduce((sum, line) => sum + line.credit, 0);
if (debits !== credits) {
throw new Error(`Entry ${entry.id} is out of balance by ${debits - credits} cents`);
}
}An entry that doesn’t balance never reaches the ledger. It’s a checksum built into the domain itself, centuries before anyone needed the word.
You Don’t Edit the Past, You Answer It
The second rule is the one that changes how you think about data. Accountants don’t edit posted entries. If an entry was wrong, you post a new one that reverses it, then post the correct one. The mistake stays in the record, along with its correction, for good.
To anyone raised on CRUD, that looks wasteful. Why not just fix the row? Because a ledger isn’t a description of the current state. It’s a history of what happened, and “we recorded the wrong amount on March 3 and corrected it on March 5” is something that happened. Editing the original destroys the one thing an auditor, or a confused property manager, most needs: the ability to see how the numbers got where they are.
Software engineers rediscovered this under other names: append-only logs, event sourcing, audit trails. The ledger had it first, and it has had five hundred years of very motivated people trying to break it.
Balances Are Derived
Which leads to the third idea. An account’s balance is never the stored truth. It’s computed from the journal:
const balance = (journal: Entry[], account: string): number =>
journal
.flatMap((entry) => entry.lines)
.filter((line) => line.account === account)
.reduce((sum, line) => sum + line.debit - line.credit, 0);For liability and income accounts the natural sign flips, but the principle holds. Real systems cache balances, because summing years of entries on every page load is slow. But the cache is only a cache. If it disagrees with the journal, the journal wins, and the cache can always be rebuilt from it. Reports work the same way: a Profit & Loss statement is a view over the journal, not a separate record.
Two Copies, on Purpose
Bank reconciliation is the part that seems to break a rule I wrote about early on: keep one copy of every fact, because two copies drift. Reconciliation deliberately keeps two. The books say what we think happened to the money. The bank statement says what the bank thinks happened. Reconciling means matching them line by line and explaining every difference.
The resolution is in how the copies are made. Two copies maintained by the same process drift silently, because one mistake lands in both or in neither. Two copies produced independently, by different people and different systems, catch each other’s mistakes when you compare them on purpose. Duplication isn’t the bug. Unexamined duplication is.
Inheriting Five Centuries of Debugging
It’s humbling to build a feature and find that its hardest design decisions were made in the fifteenth century. It’s also reassuring. The rules of double-entry weren’t designed for elegance. They survived because businesses that ignored them lost money and couldn’t explain why. When a data model has been tested that hard, the job isn’t to improve on it. It’s to implement it faithfully and let the code inherit five hundred years of debugging.