Practice Problem: Design a Digital Wallet
Prompt: Design a digital wallet (like a Careem Pay / PayPal / Venmo balance) where users can top up,
hold a balance, send money to other users, and withdraw. Attempt it cold for 45 minutes before reading the
solution.
Tests: balance correctness under concurrency, idempotent transfers, the ledger model, strong
consistency. A close cousin of the payment system.
Solution outline
1. Requirements
- Functional: top up (add money), check balance, transfer to another user, withdraw, transaction
history.
- Non-functional: 🚨 correctness above all — never create/lose money, never double-spend, never
double-process a retried request. Strong consistency, durability, auditability. Availability is secondary
to correctness (CP).
- Out of scope: KYC, currency conversion, external bank rails (assume a payment provider handles top-up/
withdrawal).
2. Estimation
Millions of users, thousands of transfers/sec at peak. Data is small but must be perfectly durable and
consistent. The challenge is correctness under concurrency, not raw scale.
3. Core design — a ledger, not a balance field
🚨 Don’t store balance as a mutable number you increment. Use a double-entry, append-only ledger:
every transaction is two entries (debit one account, credit another) summing to zero. Balance is derived
from (or a cached running total updated atomically with) the ledger entries. This makes correctness
checkable and gives a full audit trail.
Transfer $50 A→B:
A: -50 (debit) B: +50 (credit) sum = 0 ✓ (one atomic transaction)
4. Correctness under concurrency
- A transfer is a single ACID transaction: check A’s balance ≥ 50, write both ledger entries, update
both running balances — atomically, with row locking so two concurrent transfers from A can’t both pass
the balance check and overdraw. (Transactions)
- 🚨 Idempotency: every transfer carries a client idempotency key; a retried request returns the
original result instead of transferring twice. Essential — networks force retries.
(Idempotency)
5. Deep dives
- Insufficient funds: the balance check and debit happen in the same transaction; if balance < amount,
abort. No overdraw possible.
- Cross-shard transfers: if A and B live on different DB shards, a single transaction can’t span them →
use a saga (reserve from A, credit B, compensate on failure) with idempotency, or keep the ledger in
one strongly-consistent store so a transfer is one transaction. Prefer the latter for a wallet.
(Saga)
- Top-up / withdrawal: integrate an external provider asynchronously; the money isn’t in the wallet
until the provider confirms (PENDING → SUCCEEDED), with reconciliation. Same as the
payment system.
- Consistency stance: CP — if unsure, refuse rather than risk a wrong balance.
6. Trade-offs
| Decision | Chosen | Why |
| — | — | — |
| Balance | Double-entry ledger | Auditable, checkable, no lost updates |
| Transfer | Single ACID transaction + row lock | No double-spend under concurrency |
| Retries | Idempotency key | Networks force retries; no double transfer |
| Consistency | CP (correctness over availability) | A wrong balance is worse than downtime |
7. What a strong answer includes
The ledger as the correctness foundation, idempotency keys for transfers, atomic balance-check-and-debit to
prevent overdraw/double-spend, explicit CP stance, and reconciliation for external top-up/withdrawal. A weak
answer stores a mutable balance and updates it without idempotency or atomicity — vulnerable to double-spend
and double-processing.
Further reading