Double-entry ledger
Double entry records each transfer as a debit and a credit that sum to 0. A balance is the sum of its entries, so you can rebuild and check it at any time. A cached balance that lives for 300 s can stay wrong for 5 minutes.
See it on the board
The same traffic runs on two boards. The simulator measured both when this page was built.
| Measure | Goal | Cache keeps old balances for 300 s | Ledger changes update the cache |
|---|---|---|---|
| Data mismatches (Balances) | under 2,500 | 199,108 Fail | 1,813 Pass |
| p99 latency (Balances) | under 50 ms | 23 ms Pass | 24 ms Pass |
| Error rate (Balances) | under 0.1% | 0% Pass | 0% Pass |
| Monthly cost | 24 USD/mo | 28 USD/mo |
Key numbers
| Number | Value |
|---|---|
| Entries per second, one ledger | 15,000 ops/s |
| Ledger write, median | 0.5 ms |
| Default checkpoint interval of a change feed | 30 s |
Outbox · 10 orders · 10 events · 0 lost
How it works
Money does not appear or vanish. A transfer moves value from one account to another. A double-entry ledger records this as two entries: a debit on one account and a credit on the other. The two entries are written together in one transaction, and they always sum to zero. If they do not, something is wrong, and the ledger can say so.
A balance is not a stored fact. It is the sum of the entries of the account. This helps in two ways. You can rebuild any balance from the entries and compare it with the stored value. And you never overwrite history: a mistake is fixed with a new pair of entries, not by changing an old one.
In the mini board 400 transfers/s post to the ledger, and a wallet reads 8,000 balances/s through a cache that keeps entries for 300 s. A cached balance can stay wrong for 5 minutes after a transfer. The run counts the reads that do not match the ledger.
The cache needs the changes. A change feed reads each new ledger entry and updates the cached balance within a short time. Then the cache is fast and still right, and the number of mismatched reads falls below the limit of 2,500.
Keep the total of all entries as a control. It must always be zero.
In the levels you send ledger changes to the cache and check the mismatched reads.