Skip to main content
Most systems store state and hope it stays correct. A ledger inverts that: it is built so that incorrect states cannot be represented at all. Each invariant below is a constraint that makes one kind of money corruption impossible, written as a guarantee followed by the bug class that becomes possible the moment the invariant does not hold.

The root invariant: the ledger is always replayable

There is one principle underneath all the others. The postings are the only source of truth, and every balance, statement, and report is a deterministic function of them. State is never authoritative on its own. It can always be thrown away and reconstructed by replaying the postings, and the reconstruction is always identical. If that holds, you can always answer “is this number real?” by recomputing it from the entries. Every other invariant on this page exists to keep the log clean enough that the replay is always exact: the entries must be immutable (so the past does not move), value must be conserved (so the total is meaningful), writes must be atomic and idempotent (so the log has no torn or duplicated entries), and history must be deterministic (so two replays agree). Lose replayability and the ledger stops being a system of record and becomes just another database you have to trust. The invariants below are grouped by what they protect: the shape of the log, the conservation of value, the way writes happen, and the ability to reconstruct and reconcile.

Structural invariants: the shape of the log

Entries are immutable and append-only

A posting’s economic meaning never changes once written. You correct by appending a reversal or refund that references the original, never by editing it. The economic columns (amount, direction, currency, account_id) are frozen by a BEFORE UPDATE trigger; only a commit-pending flag and the reconciliation fields may ever change. What breaks if it fails: the past becomes editable. An audit can no longer trust that an entry says today what it said when it was written, a dispute has no fixed record to point at, and replay produces a different history every time you run it. Append-only is what makes the log a record rather than a guess.

Live and test data never interleave

Each OAuth client is bound to one mode, and live and test data are segregated at the storage level. The mode of your token decides which dataset you touch. What breaks if it fails: test traffic contaminates real balances. A load test or a demo could inflate a production total, and you could no longer tell synthetic movements from real ones.

Amounts are integer minor units, and currency is immutable

Every amount is stored as an exact integer in the currency’s smallest unit (cents, satoshis, or the base unit of an 18-decimal token); floats are rejected at the schema. An account’s currency is set at creation and can never change. What breaks if it fails: floating point reintroduces sub-cent drift that accumulates and never reconciles, and a mutable currency would silently reinterpret every historical posting on the account, changing what past entries mean after the fact. The same exactness applies on the wire: every monetary field is serialized as a JSON string of integer minor units. Parse them with a big-integer type.

Financial invariants: conservation of value

These are the accounting laws, and between them they make value a conserved quantity: it moves, and it cannot appear or vanish.

Debits equal credits, per currency

Every transaction balances in every currency it touches. Value is only ever moved between accounts, never created or destroyed. This is checked in application code and again by deferred Postgres triggers that fire at COMMIT, so a multi-posting transaction is validated as a whole. What breaks if it fails: money is conjured or lost. The trial balance stops summing to zero, no total can be trusted, and you are left unable to prove that the funds you report actually exist.

Balances are derived, never stored

A balance is a function of the postings beneath it. There is no balance column. A hot account keeps a running total in memory as a cache, but the postings are always the authority; if the cache and the postings ever disagreed, the postings win. What breaks if it fails: a stored balance becomes a second source of truth that can drift from the entries it claims to summarize. You end up with two numbers that disagree and no principled way to say which is real. A derived balance cannot drift, because there is nothing to drift from.

Execution invariants: how writes happen

Transactions are atomic

A transaction and all of its postings commit together or not at all. The coordinator writes them inside a single database transaction; if any part fails, including the balance check at COMMIT, the whole write rolls back. What breaks if it fails: a half-written transaction lands, with some postings present and their counterparties missing. That single torn write violates conservation directly: the ledger no longer balances, and the gap is permanent.

Operations are idempotent

Each (ledger_id, idempotency_key) maps to exactly one transaction forever, enforced by a unique index. A replay returns the original transaction and a replayed marker, and because the database enforces uniqueness, a race between two identical requests still yields exactly one transaction. What breaks if it fails: a retry under failure double-posts. This is the classic duplicate-charge bug: the network times out, the client retries, and the same economic event is recorded twice.

Writes to an account are serialized

Every active account is served by a single process that serializes the writes to it, so concurrent operations on one account apply in a defined order rather than racing. What breaks if it fails: balance-dependent decisions race. Two concurrent debits could both read a sufficient balance and both succeed, driving a non-negative account below zero. Serialization is what makes “check the balance, then post” safe under concurrency.

Reconciliation invariants: the log proves itself

Replay is deterministic

Recomputing balances and reports from the postings always produces the same result. This is the root invariant in practice, and it holds over the postings, which is why the postings are the thing to recompute from. What breaks if it fails: an audit run today and the same audit run tomorrow over unchanged data disagree. You can report a number but you cannot prove it, which is the one thing a system of record exists to do. The events stream (GET /v1/events) is a durable, replayable tail of state changes, and it is a weaker guarantee than the postings. An event is written just after its transaction commits rather than inside the same transaction, so a crash in that window can lose one event without losing the transaction. Rebuild a downstream view from events; settle a dispute from postings.

Point-in-time history is stable

Historical balances and statements are computed from each posting’s value_date, when the movement is economically effective, not from when the row happened to be inserted. The same question at the same instant always returns the same answer. What breaks if it fails: “what was my balance on March 1” has no stable answer. Restating a period later, or inserting a backdated correction, would shift historical balances depending on insertion timing rather than economic date.

Why this framing matters

None of these is a feature you switch on. They are constraints that take whole categories of failure out of the space of things that can happen at all: no torn writes, no duplicated charges, no drifting balances, no editable history. You get to reason about your integration in terms of what the ledger makes impossible, which is a stronger position than trusting that it usually does the right thing.

Next steps

Why not just use Postgres?

What you would have to build to enforce these invariants yourself.

Money flows

How value moves through accounts as balanced postings.

Reconciliation

The replay and audit invariants put to work.

Core concepts

Double-entry, corrections, idempotency, multi-currency, value dates.