Skip to content
System design course
Ch.4 · Designing real systems·concept ·7 min read

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.