Digital Wallet (PayPal / Paytm)#
Problem statement (interviewer prompt)
Design a digital wallet (PayPal / Paytm): users top up, send money to friends, pay merchants, and withdraw to a bank. Multi-currency, multi-rail (UPI / ACH / SEPA / cards). Strict ledger correctness - no funds appear or disappear - even with concurrent transfers.
flowchart LR
U([User])
WAL[Wallet Service]
LED[(Ledger)]
BANK[Bank rail]
CARD[Card network]
MERCH[Merchant API]
U --> WAL
WAL --> LED
WAL --> BANK
WAL --> CARD
WAL --> MERCH
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class U client;
class WAL,BANK,CARD,MERCH service;
class LED datastore;
flowchart TB
subgraph Apps
APP([Mobile wallet])
WEB([Web])
end
subgraph Edge
CDN
GW
end
subgraph Core
USERS([User / KYC])
LINK[Link bank / card]
BAL[Balance Service]
TX[Transaction Service]
CASHIN[Top-up / Cash in]
CASHOUT[Cash out / Withdraw]
P2P[Send to friend]
MERCH[Pay to merchant]
BILL[Bill pay]
RECUR[Subscriptions]
end
subgraph Ledger[Ledger - source of truth]
DBLEDGER[(Double-entry ledger)]
ACCT[(Per-user account / sub-accounts)]
LOCK[Pessimistic / OCC locking]
JRN[Journal]
end
subgraph Rails[External rails]
UPI
SWIFT
ACH
SEPA
CARD[Card network]
NEFT_IMPS
end
subgraph Risk
RISK[Fraud detection]
LIMIT[Velocity limits]
KYC[KYC / AML / sanctions]
PEP[PEP screening]
end
subgraph Compliance
AUD[Audit log immutable]
REG[Regulator reporting]
HOLD[Funds hold rules]
end
subgraph Webhooks
EVT[[Event bus]]
OUT[Outbound webhooks]
end
Apps --> CDN --> GW --> Core
Core --> Ledger
Core --> Rails
Core --> Risk
Core --> Compliance
Core --> Webhooks
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class APP,WEB,USERS client;
class LINK,BAL,TX,CASHIN,CASHOUT,P2P,MERCH,BILL,RECUR,LOCK,JRN,CARD,RISK,LIMIT,KYC,PEP,REG,HOLD,OUT service;
class DBLEDGER,ACCT datastore;
class EVT queue;
class AUD obs;
Ledger correctness#
- Every transfer = 2 entries (credit + debit) in atomic transaction.
- Use SERIALIZABLE or SI with retry on conflict.
- Never store balance as a single mutable row - compute or maintain via summary table with version.
Send-money flow#
- Idempotent
POST /transfer { from, to, amount, idem_key }. - Validate KYC + balance + limits.
- Lock both accounts (canonical order to avoid deadlock).
- Insert journal entries.
- Emit event.
- Notify both users.
External rails#
- Each rail has its own latency + cutoff + reconciliation file.
- Pending → settled state machine; reconcile statements daily.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
CDN | edge caching for static assets | cdn |
HLD |
MVCC & isolation levels | snapshot isolation, serializability, vacuum | mvcc-isolation-levels |
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
LLD |
State machines | FSM, HSM, transitions, guards | state-machines |
LLD |
Behavioural patterns | Strategy, Observer, State, Command, Chain | behavioral-patterns |
LLD |
Concurrency primitives | mutex, semaphore, RW lock, atomic, CAS | concurrency-primitives |
LLD |
Threading & deadlocks | thread states, Coffman, lock ordering | threading-and-deadlocks |
LLD |
Immutability | immutable types, persistent collections | immutability |
Quick reference#
Functional#
- Top-up, withdraw.
- Send to friend, request money.
- Pay merchants.
- Bill pay, recurring subscriptions.
- Multi-currency.
Non-functional#
- Strict consistency on balance.
- p99 transfer < 1 s for in-network; minutes for external rails.
- 99.99% uptime.
Capacity#
- Big platforms: 100M+ active wallets, billions of txns/yr.
- Peak: tens of thousands of txns/s.
Schema#
accounts(id, user_id, currency, status)journal_entries(id, account_id, debit, credit, ref, ts)immutabletransfers(id, from_acc, to_acc, amount, status, idem_key, version)link_methods(id, user_id, type, last4, ext_ref)
Trade-offs#
- Ledger as source of truth, balances are projections.
- External rails are slow + flaky: pending state mandatory.
- PCI / banking compliance: tokenize cards, isolate scope.
- Strong consistency in-DC, eventual across-region for projections.
Refs#
- "Building Square's Money Movement Platform" talks.
- PayPal architecture posts; Paytm engineering.
- "Distributed Transactions Done Right" (Stripe blog).
- ByteByteGo "Design a wallet".
FAQ#
How does a digital wallet guarantee no money is lost?#
Every transfer is a double entry write where total debits equal total credits inside an append only ledger. Reconciliation against bank statements catches any drift quickly.
How are concurrent transfers handled safely?#
Transfers use idempotency keys so retries do not double charge. Updates run under row level locks or serialized via per account queues so two concurrent debits cannot oversell the balance.
How does a wallet integrate UPI, ACH, SEPA, and cards?#
Each rail has an adapter that translates the wallet's transfer intent to the rail's protocol. The ledger is the single source of truth and rails are settled asynchronously with retries.
How does a multi currency wallet work?#
Balances are tracked per currency. Cross currency transfers create two ledger entries plus a FX leg booked at the quoted rate. Authoritative balances are always per currency, never aggregated.
What is the difference between authorization and capture?#
Authorization reserves funds on the user's funding instrument. Capture actually moves the money. Wallets often pre authorize, then capture only after fulfillment, releasing the hold if canceled.