Digital wallets and stored balances
Designing wallets that hold money or credits: balances derived from a double-entry ledger, top-ups and withdrawals, peer-to-peer transfers, holds and authorisations, concurrency on hot balances, limits and KYC, multi-currency, reconciliation with banks, and in-app credits and virtual currencies.
Reading is half of it. See this used in a real interview: walk through Design a Payment System →
PayPal and Venmo balances, ride-sharing and delivery credits, prepaid transit cards, game currencies and AI usage credits are all stored balances: money (or money-like value) held inside your system on behalf of users. A wallet must never create or lose value, must handle concurrent spending safely, and must reconcile with the outside world. It is a frequent variant of payment interview questions.
Balances come from a ledger
Never store a balance as a single mutable number updated by arbitrary code. Record every movement as entries in a double-entry ledger:
- Each transaction has entries that sum to zero: a top-up debits a "cash at bank" account and credits the user's wallet account; a transfer debits Alice's wallet and credits Bob's.
- The balance of an account is the sum of its entries.
- Entries are append-only; corrections are new reversing entries.
For speed, keep a materialised balance per account updated in the same transaction as its entries, and verify it against the entries regularly. See payments and ledgers.
Core operations
- Top-up: charge a card or bank account through a payment provider; credit the wallet only when the payment succeeds (or mark funds as pending until settled).
- Spend: debit the wallet for a purchase, crediting a merchant or revenue account.
- Transfer: debit one user, credit another, atomically.
- Withdrawal: debit the wallet, send a payout to a bank account; handle payout failures by reversing.
Every operation carries an idempotency key, so retries never double-spend or double-credit. See idempotency keys in practice.
Preventing overspending
Two concurrent purchases must not both succeed if the balance covers only one:
- Within one database: a transaction that checks and debits atomically, for example
UPDATE accounts SET balance = balance - 30 WHERE id = ? AND balance >= 30, plus the ledger entries in the same transaction. See optimistic vs pessimistic locking. - Serializable isolation or row locks for multi-account transfers, locking accounts in a consistent order to avoid deadlocks. See transactions and isolation.
- Shard by account id; a transfer between shards needs a saga (debit with a pending state, credit, then confirm, with compensation on failure). See distributed transactions and idempotency.
Holds (authorisations)
Ride-sharing estimates a fare before the trip; the final amount comes later. Place a hold: move funds from the available balance to a held balance, then capture the final amount (and release the rest) when the trip ends, or release fully on cancellation. Holds expire automatically if never captured. The same model covers hotel deposits and pre-authorised card payments. See Design Uber.
Hot accounts
Some accounts receive enormous traffic (the platform's revenue account, a popular merchant). Updating one row for every transaction creates contention. Options: split the account into sub-accounts and sum them, batch entries for system accounts, or post to them asynchronously while keeping user-facing accounts synchronous. See hot keys and skew.
Limits, KYC and compliance
Wallets holding real money are regulated:
- KYC (know your customer) verification before allowing higher balances, transfers or withdrawals.
- Limits per transaction, day and month, depending on verification level.
- Sanctions and AML screening of transfers, with holds for review. See fraud detection.
- Customer funds often must be held in segregated bank accounts.
Multi-currency
Keep one account per user per currency; conversions are explicit transactions with a recorded exchange rate, debiting one currency account and crediting another via an FX account. Store amounts as integers in minor units with the currency code.
Reconciliation
Daily (or continuous) reconciliation compares:
- The ledger's view of money held against bank statements and payment provider reports.
- Materialised balances against summed entries.
- Pending top-ups and payouts against provider statuses.
Breaks are investigated and fixed with correcting entries, never by editing history. See data quality and data contracts.
Virtual currencies and credits
Game currencies, loyalty points and prepaid AI credits use the same ledger design even if they are not legal money: purchases credit balances, consumption debits them, expirations and refunds are entries. Usage-based consumption (tokens, API calls) is often aggregated and debited in batches. See subscriptions and recurring billing and multiplayer game servers.
In the interview
"Every wallet movement is a double-entry ledger transaction with an idempotency key; the available balance is materialised in the same database transaction with a conditional debit so concurrent spends cannot overdraw; trips use holds that are captured or released; cross-shard transfers are sagas; top-ups credit only on confirmed payment; daily reconciliation against bank and provider reports catches breaks." See Design a Payment System.
Checklist
- Balances derived from an append-only double-entry ledger.
- Materialised balances updated atomically with entries and verified.
- Conditional debits or locks to prevent overspending; sagas across shards.
- Holds with capture, release and expiry.
- Idempotency keys on every operation.
- KYC, limits, AML screening; per-currency accounts.
- Continuous reconciliation with external systems.