Status: Accepted · Phase: 1 · Supersedes: none · Superseded by: none
Context
Arc must record money movement across six currencies and two settlement rails, and answer two different questions: are the books consistent? and do we hold what we owe? A single mutable balance per account answers neither — it cannot be audited, cannot be replayed, and gives no place to record why a number changed.Decision
Classical double-entry:- Every movement is a journal of two or more entries that balance, per currency independently.
- Balances are derived by folding entries, never stored as a mutable column.
- Entries are append-only. A correction is a new, opposing journal.
- A customer’s balance is a liability; the matching float is an asset.
DEFERRABLE INITIALLY DEFERRED constraint trigger that fires at COMMIT.
Consequences
Good. Every figure is reconstructible from the entry log. The audit trail is the data, not a parallel table that can disagree with it. Duplicated enforcement means the invariant survives a migration script or apsql session during an incident — an invariant that depends on every future developer using the right class is a convention, not an invariant.
Costs. Reads are folds, so balances get more expensive as history grows; real volume needs periodic snapshots. Deferred triggers mean a violation surfaces at COMMIT rather than at the offending statement, which reads oddly until you know why.
Per-currency balance is the subtle part. A journal converting EUR to USDC has two halves and each must close on its own — offsetting one against the other would be adding quantities of different things. FX position accounts bridge them, and the pair is where an unhedged exposure becomes visible.