Revision sheet
Design a digital wallet.
The six things worth holding in your head the night before. Everything else is on the full page.
The opening move — minute one
“A wallet is a stored-value ledger, so every balance is derived from an append-only double-entry log — never a column I mutate. The contract I protect above everything: a balance can’t be spent twice and money is never created or destroyed. At 1B transfers a day I have to shard by account, so the interesting part is making a transfer atomic when the two accounts live on different shards.”
The transfer state machine
INITIATED → PENDING → POSTED → SETTLED
FAILED (from PENDING: insufficient funds / limit / decline — release the hold) · EXPIRED (from PENDING: hold TTL elapsed — sweeper releases) · REVERSED (from POSTED / SETTLED: compensating transfer — never deletion; may drive the account negative)
Must-haves
- Designs a double-entry ledger (every movement = balanced debit + credit) and explains why a single mutable ‘balance’ column is wrong.
- Prevents double-spend with an atomic conditional debit (row lock / serializable / CAS / single-writer), names the TOCTOU race in check-then-update.
- Makes a transfer atomic: same-shard = one ACID transaction; cross-shard = a saga with holds (reserve → credit → confirm) + compensation, and rejects naive 2PC for the right reasons.
- Separates available vs total balance with holds, and names the transfer state machine (INITIATED → PENDING → POSTED → SETTLED) and what triggers each transition.
- Sizes throughput (~11.6K TPS avg, ~35K peak, 100K+ festival), ledger growth, multi-year retention — and concludes sharding by account_id.
Red flags
- A single mutable ‘balance’ column instead of a ledger — corrupts under any concurrent write or partial failure.
- Check-then-debit with no atomicity — two concurrent transfers double-spend the same balance.
- Crediting the destination before the source is debited, or two separate transactions for a transfer — money created / destroyed on crash.
- Treating a card / ACH top-up as final, spendable money — a provisional-credit loss waiting to happen.
The ten trap claims — each one is wrong
- “a wallet is just a balance column you increment and decrement”
- “check the balance, then debit if it is sufficient”
- “a transfer is one UPDATE for the sender and one for the receiver”
- “just use distributed transactions (2PC) across shards”
- “available balance equals total balance”
- “a top-up is instantly spendable money”
- “eventual consistency is fine for balances”
- “reverse a transfer by deleting its ledger rows or subtracting from the balance”
- “one big Postgres is fine — sharding is optional”
- “publish the event to Kafka, then write the ledger”
The L4 lines — what depth sounds like
Double-Spend / Concurrency Control
Compares pessimistic locking vs optimistic CAS vs single-writer-per-shard under contention, quantifies retry / abort rates at peak, and reasons about isolation-level anomalies (write skew) on the money path
Double-Entry Ledger
Quantifies ledger write volume (~11.6K TPS × 2+ entries), shards by account_id, mitigates hot-account write contention via sub-accounts + batching, and handles cross-currency entries with rate snapshotting
Atomic Cross-Shard Transfer
Addresses the exact half-commit (credit lands, source confirm fails) with a recovery worker, stuck-saga TTL sweeper, compensation ordering, and bounds the worst case to funds held for the hold TTL
Holds & Available Balance
Quantifies hold volume and sweeper cadence, handles partial captures and hold top-offs, and reasons about hold-vs-authorization semantics for pending withdrawals
Provisional Credit & Settlement Risk
Quantifies float exposure and reserve sizing, designs negative-balance recovery (dunning / offset / write-off), and sets availability policy by user risk tier and funding-source type
Reconciliation & Money Conservation
Quantifies reconciliation load across shards and PSPs, replays the append-only ledger for residuals, and defines regulatory reporting for unresolved mismatches and recovered negative balances