Designing a digital wallet
Hold balances and move money between users — atomic balance updates, a double-entry ledger, and idempotent transfers that never lose or duplicate funds.
The problem
Design a digital wallet (PayPal/Venmo/Paytm balance): users hold a balance, top up from a bank/card, pay merchants, transfer to other users, and withdraw. The core is correct, concurrent balance management — no negative balances, no lost or duplicated money — built on the ledger + idempotency foundation from the payment lesson.
Step 1 — Requirements
Functional: view balance; top up; pay / transfer to another user or merchant; withdraw; transaction history.
Non-functional: strong consistency (balances always correct, no double-spend), durability (never lose a transaction), idempotency (retry-safe), high availability, auditability.
Step 2 — The double-entry ledger (reuse)
A wallet balance is just an account in a double-entry ledger (payment lesson). Every movement posts balanced entries:
top up $50: DEBIT bank_clearing 50, CREDIT alice_wallet 50
Alice pays Bob: DEBIT alice_wallet 20, CREDIT bob_wallet 20
The balance is derived from (or maintained alongside) the ledger — never a free-floating number you mutate carelessly. Append-only entries make it auditable; a refund is a reversing entry.
Step 3 — Atomic balance updates (no double-spend)
The correctness crux: two concurrent payments from Alice must not both succeed if she can’t afford both. Update atomically with a balance check:
-- succeeds only if sufficient funds; 0 rows → reject (insufficient balance)
UPDATE wallets SET balance = balance - :amt
WHERE user_id = :alice AND balance >= :amt;
This conditional decrement (same shape as inventory) prevents overdraft under concurrency. A transfer debits the sender and credits the receiver in one transaction so it’s atomic — both sides move or neither does.
Step 4 — Idempotency
Every transaction carries an idempotency key so retries (network, double-tap) don’t move money twice — return the original result on replay (payment lesson). Essential for a mobile app on flaky networks.
Step 5 — Top-up and withdrawal (external money)
Top-up/withdraw bridge to external banks/PSPs, which are async — model them as a
state machine (initiated → pending → completed/failed) advanced by webhooks, and only
credit the wallet once the external leg confirms (don’t credit on “initiated”). Reuse the
payment system here.
Step 6 — Architecture
app → wallet API (idempotency key) → wallet service
→ ledger (double-entry, transactional balance updates)
→ external bridge (top-up/withdraw via PSP/bank, async webhooks)
→ transaction history (read model, derived from ledger)
Trade-offs to raise
- Maintained balance column (fast reads, must stay in sync) vs sum-the-ledger (always correct, slower). Usually a materialized balance updated in the same transaction as the ledger entries.
- Strong consistency (CP) over availability — never risk a wrong balance.
- Single account hotspot — a very active wallet (a big merchant) contends on its row; shard sub-balances or batch.
The interview cue
“A wallet is an account in a double-entry ledger; payments/transfers post balanced entries in one transaction with a conditional atomic decrement (no overdraft, no double-spend); every operation is idempotent; top-up/withdraw bridge to banks via an async state machine, crediting only on confirmation. It’s CP with a ledger as the auditable truth.” Atomic balance updates + ledger + idempotency is the answer; implementation next.