SysDesignPrep.com
Study guide 160 of 183

Money in system design: ledgers, idempotency and reconciliation

How to handle money correctly: integer minor units, double-entry ledgers, balances derived from entries, idempotent payment calls, the unknown-outcome state, authorisation and capture, and daily reconciliation with providers.

Reading is half of it. See this used in a real interview: walk through Design a Payment System →

Any system that charges, pays out, holds credits or tracks balances has to get money exactly right, and interviewers hold money to a higher standard than anything else: a duplicated like is a curiosity; a duplicated charge is a support ticket, a chargeback and possibly a regulator. A few rules, applied consistently, cover most of what can go wrong.

Representing amounts

  • Store amounts as integers in minor units (cents, pence): 4999 means $49.99. Never floats: 0.1 + 0.2 is not 0.3 in binary floating point, and rounding errors accumulate.
  • Always store the currency next to the amount; never add amounts in different currencies.
  • Be explicit about rounding when splitting (fees, taxes, splitting a bill three ways), and put the leftover cent somewhere deliberate.
  • Some currencies have no minor unit (JPY) or three decimal places (KWD); use a currency table, not an assumption of two decimals.

Double-entry ledgers

A ledger records every movement of money as entries that sum to zero: money leaves one account and enters another. A $50 payment with a $1.50 fee might be:

AccountDebitCredit
customer_card_receivable50.00
merchant_balance48.50
platform_fee_revenue1.50

Rules:

  • Every transaction’s entries balance; the database enforces it (a check in the transaction that writes them).
  • Entries are append-only: mistakes are fixed with new reversing entries, never by editing history.
  • Balances are derived from entries (sum of an account’s entries), cached in a balance row updated in the same transaction, and verifiable at any time by re-summing.

Why not just a balance column? Because a single column cannot explain itself: when it is wrong, you cannot tell why, and concurrent updates silently lose money. A ledger is an audit trail by construction. See event sourcing for the general idea.

Concurrency on balances

Two withdrawals racing against one balance must not both succeed if together they overdraw it. Use a conditional update in the same transaction as the entries (UPDATE balances SET amount = amount − 3000 WHERE account = ? AND amount >= 3000), or lock the balance row for the short transaction. See transactions and isolation. For extremely hot accounts (a platform’s fee account), split them into sub-accounts and sum on read.

Idempotency everywhere

Networks fail after the money moved but before you heard about it. Every money-moving operation must be safe to retry:

  • Clients send an idempotency key with each payment request; the server stores the key with the result and returns the same result on retry. Stripe keeps keys for 24 hours.
  • Your calls to the payment provider also carry an idempotency key (usually your payment id), so your own retries cannot double-charge.
  • Webhook handlers deduplicate by event id. See designing webhooks.

The unknown outcome

The hardest state in payments: you sent a charge request and the connection timed out. Did the customer get charged? You do not know.

  • Record the payment as pending/unknown, never as failed (a failed state invites the user to try again and get charged twice).
  • Resolve it by querying the provider with your idempotency key or payment id, by retrying the same request with the same key, or from the provider’s webhook.
  • Show the user "processing" until resolved.

Model payments as a state machine (created → authorised → captured → refunded, with failed and unknown branches), with conditional transitions so duplicates and races are no-ops. See Design a Payment System.

Authorise, capture, refund

Card payments are two steps: authorise (reserve funds on the card) and capture (actually take them), with up to about seven days between. Use this deliberately:

  • Authorise at checkout, capture when the goods ship or the service is delivered.
  • If the order fails or is cancelled before capture, void the authorisation (free and instant) instead of refunding (fees, days to appear).
  • Marketplaces (Airbnb, DoorDash) authorise at booking and capture later. See Design Airbnb.

Reconciliation

Your records and the provider’s will disagree sometimes: a webhook missed, a timeout resolved differently, a manual refund in the provider’s dashboard. Daily reconciliation compares your ledger with the provider’s settlement reports line by line, flags mismatches, and routes them to a person or an automated fix. It is how every real payments team finds the bugs that tests missed. Also reconcile the ledger against itself: every transaction balances, every cached balance equals the sum of its entries.

Payouts and money held for others

When you hold money for sellers or drivers, the ledger tracks what you owe them; payouts move it out on a schedule, with holds for refunds and chargebacks. Payouts are idempotent batch jobs with their own reconciliation against bank reports. See background jobs.

Security and compliance

Never store raw card numbers; use the provider’s tokenisation so your systems stay out of most PCI scope. Restrict and audit who can move money, require two people for manual adjustments, and keep the ledger immutable for audit.

Checklist

  • Integer minor units plus currency; explicit rounding.
  • Double-entry, append-only ledger; balances derived and checked.
  • Conditional updates for concurrent debits.
  • Idempotency keys from client to provider; webhooks deduplicated.
  • An unknown state resolved by querying, never assumed failed.
  • Authorise then capture; void before refund.
  • Daily reconciliation against provider reports.

Open in your browser to sign in

Google does not allow sign-in inside this app's built-in browser. Open this page in Safari and sign in there. The link opens this same page.

Tap the ⋯ or share button at the top or bottom of the screen, then Open in browser. Or copy the link and paste it into Safari.