#37 Double-Entry Bookkeeping
Wire Transfer API
An API that moves money between accounts, where the books must always balance and a retried request must never pay twice.
What is being asked
Design a transfer API for a bank or payments company. The centre of the answer is double-entry bookkeeping and how to keep it correct under retries, crashes and concurrency.
Requirements
Functional
- Transfer an amount from one account to another; query balance and history.
- Transfers to other banks through external payment rails.
Non-functional
- Money is never created or destroyed.
- Exactly-once effect for each transfer request.
- A complete audit trail; high availability.
Design
- Each request carries an idempotency key, stored with a unique constraint. A repeated key returns the original result.
- A transfer is recorded as two ledger entries in one database transaction: a debit on the source and an equal credit on the destination. The entries of a transaction always sum to zero.
- In the same transaction the source balance is checked and updated, with a row lock or a conditional update so it cannot go below zero.
- Ledger entries are never changed. A mistake is corrected by a new reversing entry.
- For an external transfer, the money first moves to a holding account, and a message in an outbox table (written in the same transaction) instructs the connector to call the other bank.
Key decisions and trade-offs
- Ledger as truth, balance as cache. Balances can always be recomputed from entries; a nightly check that they agree catches bugs.
- Lock ordering. Lock the two accounts in a fixed order, by id, so two opposite transfers cannot deadlock.
- Transactional outbox avoids "database committed but message not sent". The connector must itself be idempotent, since messages are delivered at least once.
- Integers, not floats. Store amounts as whole minor units with a currency.
Follow-up questions
- How would you transfer between accounts held in different database shards?
- What happens when the external bank does not reply?
- How do you handle a transfer between two currencies?
- How would you detect that the books no longer balance?