Every financial system eventually rediscovers double-entry bookkeeping. Not because engineers read accounting textbooks, but because the alternative — tracking balances as a single number — produces bugs that are impossible to audit and reconcile. Double-entry is a 700-year-old invention that turns out to be exactly the right data model for distributed financial systems.

The Core Principle#

Every transaction touches exactly two accounts: a debit on one, a credit on the other of equal amount. If Alice sends Bob $50: debit Alice’s account $50, credit Bob’s account $50. The sum of all debits equals the sum of all credits. Always. This invariant is checkable at any time and any discrepancy points directly at the corrupt transaction.

Compare to the single-balance approach: UPDATE accounts SET balance = balance - 50 WHERE user = 'alice' then UPDATE accounts SET balance = balance + 50 WHERE user = 'bob'. If the first update succeeds and the second fails, $50 vanishes. You need application-level logic to detect and recover. With double-entry, the missing credit is structurally visible.

Ledger as Immutable Log#

Store transactions as an append-only ledger, not as mutable balance rows. Each entry: (transaction_id, account_id, amount, type, timestamp). Balance is a derived value: SELECT SUM(CASE WHEN type='credit' THEN amount ELSE -amount END) FROM ledger WHERE account_id = X.

graph TD A[Transfer: Alice sends Bob $50] --> B[Ledger entry 1: DEBIT Alice $50, txn_id=abc123] A --> C[Ledger entry 2: CREDIT Bob $50, txn_id=abc123] B --> D[Balance check: SUM debits = SUM credits for txn_id=abc123] D --> E{Balanced?} E --> |Yes| F[Transaction complete] E --> |No| G[Integrity error: alert and investigate] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Atomicity Across Accounts#

Both ledger entries must be written atomically. In a single database, this is a two-row insert in one transaction. In a distributed system where Alice and Bob are on different shards, you need two-phase commit or a saga pattern: write the debit, then the credit, with a compensating transaction (reverse debit) if the credit fails.

The immutable ledger makes compensations explicit and auditable: rather than rolling back, you write a new correcting entry. The full history of debits, credits, and compensations is preserved.

At Oracle#

Salesforce billing uses a ledger model internally for invoice adjustments. When a customer receives a credit (discount, refund), it’s a ledger entry against the invoice, not a mutation of the original invoice amount. This meant every account balance was reconstructable from the ledger history, which was critical for SOX compliance audits. Auditors could trace every cent from initial charge through every adjustment.

What I’m Learning#

The immutable ledger with double-entry is the right default for any financial data. The derivability of balance from the ledger means you can recompute it at any point in time, audit every change, and detect corruption structurally rather than by comparing to an expected value.

Have you migrated a mutable-balance system to a ledger model, and what surprised you in the process?