Move money without losing the other half
Two entries, one transaction, and a receipt that makes a retry safe.
Double-entry books that stay balanced.
Python / Postgres / HTTP
Notes & checks
A double-entry ledger on Postgres, with an HTTP API and an invariant checker. Recorded test suite: 113 tests.
The problem
Imagine a payment succeeds, but the response gets lost. Clicking again should not move the money twice. And if the server stops halfway through, one account should not lose money without the other receiving it. ledger tackles both problems with Postgres and double-entry bookkeeping.
How it works
- Alice: −125¢Debit posting
- One transactionSame transfer receipt
- Bob: +125¢Credit posting
Name the request
The caller supplies an idempotency key, a receipt name for one intended transfer. The database reserves that key with the source, destination, and amount. An identical retry returns the original transfer. Reusing the key for a different request fails.
Move both sides together
The writer locks the accounts in a stable order, checks the balance rules, then writes two opposite postings and updates the balances. A posting is one account's side of a transfer. All of this happens in a transaction, so it commits together or rolls back together.
Check the books independently
A separate checker compares balances with postings, checks that transfers have the right pair of entries, and looks for broken references and duplicate keys. Its read-only snapshot sees one consistent view even while other requests write.
Decisions & tradeoffs
Count cents, not fractions
Amounts are integer cents, avoiding floating-point rounding. Each database represents one currency. Normal accounts cannot go negative; a clearing account can explicitly allow it for funding.
Audit without repairing
The checker reports violations rather than changing records. It loads a complete snapshot into memory, which keeps the checks straightforward but makes memory use grow with the ledger.
Try it yourself
With Python 3.12+ and uv, install a fresh checkout and try the offline checker. This example uses empty account lists and never connects to Postgres.
uv sync --group dev
uv run --no-env-file python - <<'PY'
from ledger import check_snapshot
print(check_snapshot([], [], []).to_dict())
PYThis checks an explicitly supplied empty snapshot. It does not prove a database is initialized or healthy. For real transfers, the README has a disposable, local-socket Postgres demo and cleanup commands.
Sources & scope
Based on the local README, transaction writer, and independent checker. The diagram is an example, not a measured result.