module-21/double-entry-ledger.mdPRODUCTION MODULE

The Double-Entry Ledger

Money is never overwritten. It only moves — and every move is permanent.

A balance stored as a single number, overwritten in place — no history, no audit, and a race condition away from spending the same money twice.
Bank-grade double-entry bookkeeping: every movement is a debit and a credit, rows are never updated or deleted, the balance is derived from immutable history.

The incident

A wallet balance stored as one integer column. Two concurrent spends read the same balance, both passed the check, both wrote. The user spent the same money twice, the books didn't balance, and there was no history to reconstruct what happened — because history was never kept.

Why the obvious approach fails

balance = balance - amount looks like arithmetic but is actually three operations — read, compute, write — with a race between them. Locks patch the race but not the deeper flaw: a mutable balance has no memory. You can't audit it, can't reconcile it, can't answer a regulator's 'why is this number what it is,' and every bug that touches it destroys information permanently. Banking solved this five hundred years ago: don't store the balance, store the movements. The balance is a derived fact.

The standard

  • Every movement of value is two entries — a debit and a credit — that must sum to zero; the invariant is enforced at the transaction level.
  • Ledger rows are append-only. No UPDATE, no DELETE — corrections are compensating transactions, so the audit trail is complete by construction.
  • Balances are derived from entry history (with cached snapshots for performance), never stored as independently mutable state.
  • Every entry carries a correlation ID tying it to the business event (order, refund, payout) that caused it.
  • Reconciliation against payment-provider records runs on a schedule, and discrepancies alert — they don't wait for month-end.

How Cortex validates it

At plan time

Any blueprint touching stored value must model it as ledger entries; a mutable balance column in the schema fails plan review.

At validation

assert: ledger_tables.grants exclude UPDATE, DELETEassert: sum(transaction.entries) == 0 for all transactionsassert: derived_balance == sum(entry_history)

Assertion syntax is illustrative.

At review

The approver sees the concurrent-spend test and the zero-sum verification across the seeded transaction set.

Composes with

See where your codebase stands against this standard.