Design problems
Payments & fintechMediumSDE-2 35 min

Design a digital wallet (Paytm)

Asked at
AmazonCREDGoldman SachsPaytmPhonePeRazorpay

The interviewer asks

“Design a prepaid digital wallet like Paytm Wallet or Amazon Pay balance. Users add money from UPI, cards or netbanking, pay merchants, send money to other wallet users, receive cashback and withdraw to their bank. How do you keep every balance correct with millions of concurrent payments?”

What a strong answer looks like

A strong answer notices that, unlike UPI, here we are the bank for the wallet balance: we hold the money (in an escrow account, as a regulated prepaid payment instrument issuer) and we own the ledger. So the core is a double-entry ledger where every movement is a balanced journal between accounts (user wallets, merchant wallets, escrow cash, cashback expense, fees), and wallet debits are atomic conditional updates that never allow a negative balance. Add money is a two-phase flow with the payment gateway (credit only on confirmed payment, idempotent on the gateway reference). Payments and P2P transfers are single-database transactions when both accounts are on the same shard, and a saga with a holding account when they are not. Regulatory limits (KYC tiers: minimum-KYC wallets capped at ₹10,000 balance, full-KYC up to ₹2 lakh) are enforced in the same transaction. Deep dives: the ledger and balance correctness, cross-shard transfers, hot merchant accounts, and the daily invariant that the escrow bank balance equals the sum of all wallet balances.

Concepts to mention

double-entry ledger with balanced journalsconditional debit (balance >= amount)idempotency key on every money movementescrow balance equals sum of wallets invariantKYC tier limits enforced in the transactionsaga with clearing accounts for cross-shard transfershot merchant account splitting / batchingpromotional balance as expiring lotstop-up credited only on confirmed paymentreversing journals instead of edits

1 Requirements

Functional

  • Add money to the wallet via UPI, cards or netbanking through a payment gateway.
  • Pay merchants from the wallet (QR, online checkout) and send money to other wallet users.
  • Show balance and statement; split balance into main and promotional (cashback) parts.
  • Credit cashback and promotional balance, with expiry for promotional money.
  • Refund merchant payments back to the wallet.
  • Withdraw to a bank account for full-KYC users.
  • Enforce KYC limits on balance and monthly loading.

Non-functional

  • Balance is never negative and never double-spent; every rupee is traceable in the ledger.
  • Wallet payment p99 under 200 ms (no external call on the pay path).
  • 99.99% availability for payments; strong consistency for balances.
  • Peak ~20k wallet transactions/s during sales and cashback campaigns.
  • Daily invariant: escrow bank balance = sum of all wallet balances (+ in-flight).
  • Audit trail retained for years; data stored in India.

Out of scope (say it out loud)

  • KYC verification process itself (Aadhaar/video KYC).
  • Merchant onboarding and acquiring for cards/UPI.
  • Credit lines and pay-later products.

2 Estimates

Wallets~100M wallets, ~20M daily activeAssumed scale for a large Indian wallet.
Wallet transactions~40M/day ≈ 460/s avg, ~20k/s peakPayments, transfers, cashback and refunds. Flash cashback campaigns cause sharp spikes.
Ledger lines~150M/day~40M transactions × ~3-4 lines (debit, credit, fee, cashback).
Ledger storage~15 GB/day, ~6 TB/year150M × ~100 bytes; kept for years.
Add-money flows~5M/dayEach involves a gateway payment and a callback.
Hot merchant writesup to ~2k/s on one merchantA large merchant (food delivery, recharge, ticketing) during a sale receives a big share of all payments.

3 API

EndpointWhat it does
POST/v1/wallet/topupsStart add money {amount, method}; returns a gateway order; wallet is credited only on confirmed payment.
POST/v1/wallet/paymentsPay a merchant {merchant_id, amount, order_ref}; Idempotency-Key; returns txn_id and new balance.
POST/v1/wallet/transfersP2P transfer {to_user, amount}; Idempotency-Key.
POST/v1/wallet/refundsMerchant refunds {txn_id, amount} back to the user's wallet; idempotent.
POST/v1/wallet/withdrawalsWithdraw to bank (full-KYC only) via payout rails (IMPS).
GET/v1/wallet/balanceMain + promotional balance.
GET/v1/wallet/statement?cursor=Ledger-backed statement.

4 Data model

accountsPostgres/MySQL sharded by account_id
account_idowner_idtype (user_main|user_promo|merchant|escrow|cashback_expense|fees|suspense)currencybalance_paisekyc_tierstatusversion

Balance column is a cached sum of ledger lines, updated in the same transaction as the lines. CHECK (balance >= 0) for user accounts.

journalsSame shard as the debited account
journal_idtype (topup|payment|p2p|refund|cashback|withdrawal)idempotency_key (unique)statuscreated_at

One business transaction = one journal.

ledger_linesSame shard, append-only
line_idjournal_idaccount_idamount_paise (+credit / -debit)balance_after

Sum of amounts in a journal = 0. Never updated; reversals are new journals. balance_after makes statements and audits easy.

limitsSame shard as user account
user_idmonthloaded_paisespent_paisekyc_tier

Checked and updated in the same transaction as the debit or credit.

promo_lotsPostgres
lot_iduser_idamount_paiseremaining_paiseexpires_atcampaign_id

Promotional balance kept in lots so expiry and campaign accounting are exact.

outbox / eventsPostgres → Kafka
event_idjournal_idtypepayload

Notifications, merchant settlement, analytics and fraud consume these.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALrefundspay, transferatomic journalcollect moneywebhookcredit walletoutboxcross-shard legsescrow check, payoutsWallet appMerchant app / serverAPI gatewayTop-up serviceWallet serviceRisk + limitsKafka (journal events)Transfer saga workerRecon + settlementjobsLedger DB (sharded)Redis (limits cache)Payment gatewayEscrow bank + IMPS
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Add ₹1,000 via UPI

  1. 1User taps Add Money; top-up service checks KYC limits (min-KYC: monthly load and ₹10,000 balance cap) and creates a pending topup with a gateway order.
  2. 2User pays through the gateway (UPI intent); money lands in the wallet issuer's escrow account via the gateway's settlement.
  3. 3Gateway webhook (verified signature) reports success; top-up service posts a journal idempotent on the gateway payment ID: debit 'gateway receivable' ₹1,000, credit user_main ₹1,000.
  4. 4The user's balance and monthly loaded amount update in the same transaction; an event triggers a notification.
  5. 5If the webhook is late, the app polls topup status; a reconciler queries the gateway for pending topups older than a few minutes.

Pay a merchant ₹250 from wallet

  1. 1App sends POST /wallet/payments with an Idempotency-Key; the wallet service runs a quick risk check.
  2. 2On the user's shard, one transaction: insert journal (unique idempotency_key), UPDATE accounts SET balance = balance - 250 WHERE account_id = user AND balance >= 250 (fail if 0 rows), insert ledger lines.
  3. 3Promotional money is used first if the merchant is eligible; lots are consumed oldest-expiring first.
  4. 4If the merchant account is on the same shard, credit it in the same transaction. Otherwise credit a per-shard 'in-transit' clearing account and emit an event; the saga worker credits the merchant on its shard idempotently.
  5. 5Response returns success and the new balance in under 200 ms; merchant gets a payment notification.

Withdraw to bank

  1. 1Full-KYC user requests withdrawal of ₹5,000; wallet debits user_main and credits 'payouts in flight' in one transaction.
  2. 2Payout worker calls IMPS via the bank with an idempotent reference; on success, it moves 'payouts in flight' to 'escrow cash out'.
  3. 3On a confirmed failure, a reversal journal credits the user back; unknown outcomes wait for status checks and bank reports, never a blind retry.

7 Deep dives

Ledger and balance correctness

A wallet balance is a liability we owe the user, so it must be exact and provable. Use double-entry: every movement is a journal whose lines sum to zero across accounts. Users, merchants, the escrow cash account, cashback expense, fee revenue and clearing accounts are all accounts. Keep a balance column on each account for fast reads, but update it only in the same database transaction that inserts the ledger lines, so the cached balance and the lines can never disagree. The debit is a conditional update: UPDATE accounts SET balance = balance - x, version = version + 1 WHERE id = ? AND balance >= x. If no row changes, the payment fails with insufficient balance; there is no read-then-write race. A CHECK constraint (balance >= 0) is a second guard. Idempotency comes from a unique key on the journal, so a retried request returns the original result. Money is integer paise. Corrections are reversing journals, never edits. Continuous integrity jobs check that every journal sums to zero, that each account's balance equals the sum of its lines, and that the total of all user and merchant balances equals the escrow bank balance plus in-flight amounts, the key regulatory invariant.

Cross-shard transfers

With 100M accounts, the ledger is sharded by account_id, so a P2P transfer or merchant payment often touches two shards. Distributed two-phase commit across shards works but adds latency and blocking during failures. The common alternative is a saga with clearing accounts. Step one, on the payer's shard: debit the payer and credit a clearing account 'outgoing to shard B', in one local transaction, and write an outbox event. Step two, a worker consumes the event and on shard B debits the 'incoming from shard A' clearing account and credits the payee, idempotent on the journal ID. The payer sees success after step one because the money has left their account safely; the payee sees it within milliseconds to seconds. If step two fails permanently (payee account blocked), a compensating journal on shard A reverses the transfer. Clearing accounts across shards must net to zero once all events are processed; a monitor alerts if they do not drain. Co-locating frequently paired accounts (a user and their own promo account) on the same shard avoids sagas where possible.

Hot merchant accounts

A large merchant can receive thousands of payments a second during a sale. If every payment updates one merchant balance row, that row becomes a lock hotspot and throughput collapses to a few hundred per second. Fixes: split the merchant account into N sub-accounts (e.g. 32) and credit a random one; the merchant balance is the sum. Or credit merchants asynchronously: the payment transaction debits the user and credits a per-shard 'merchant payable' clearing account, and a batch job aggregates hundreds of payments into one merchant credit every second. Merchants rarely need sub-second balance; they need reliable notifications and correct settlement. The user's side remains synchronous and strongly consistent, which is what matters for preventing double spend.

Promotional balance and cashback

Cashback is not the same as money the user loaded. It may be usable only at some merchants, it expires, and it must be accounted as a marketing expense. Store promotional balance as lots with amount, remaining, campaign and expiry. Crediting cashback posts: debit cashback expense (per campaign), credit user_promo. Spending consumes lots in order of earliest expiry, and the journal records which lots were used so refunds can restore them correctly. Expiry is a nightly job that posts reversing journals for expired remaining amounts. Campaign budgets are enforced with a counter per campaign, decremented atomically when cashback is granted, so a viral campaign cannot overspend. Fraud teams watch for patterns like many new wallets on one device claiming cashback.

8 Trade-offs

SQL ledger vs NoSQL

Balances need transactions, conditional updates and constraints. Sharded Postgres/MySQL (or distributed SQL) gives these directly. NoSQL stores scale writes but you rebuild transactional safety. Purpose-built ledgers like TigerBeetle give very high throughput for double-entry with strict invariants.

Two-phase commit vs saga for cross-shard transfers

2PC gives atomicity but blocks on coordinator failure and adds latency. A saga with clearing accounts is non-blocking and fast but the payee is credited a moment later and needs compensations. Wallets usually choose sagas.

Synchronous merchant credit vs batched

Synchronous credit is simple and shows the merchant balance immediately but creates hot rows. Batched credit scales and is enough for merchants who settle daily.

Balance column vs computing from lines

Summing lines on every read is too slow; keeping only a balance column loses auditability. Keep both, updated together in one transaction, and verify continuously.

9 Bottlenecks

Cashback campaign spike

A big campaign brings 20x traffic in minutes. Pre-scale, protect the ledger with admission control, and make cashback crediting asynchronous so it does not compete with payments.

Hot merchant row

Sub-accounts or batched merchant credits remove the lock hotspot.

Statement queries on huge ledgers

Old lines move to cold storage; statements use an index on (account_id, created_at) and a read replica, with monthly statements pre-generated.

10 Failure handling

Gateway webhook for a top-up is lost

The top-up stays pending. A reconciler queries the gateway for pending top-ups older than a few minutes and posts the credit idempotently; the daily settlement file is the final check.

Saga worker is down

Payers are already debited into clearing accounts; payees see credits delayed. When the worker resumes it processes the outbox backlog idempotently. Monitors alert on clearing account balances that do not drain.

Database primary of a shard fails

Failover to a synchronous replica (no committed transaction lost). Payments for users on that shard fail fast for seconds and clients retry with the same Idempotency-Key.

11 What impresses

  • Explains the difference from UPI: a wallet issuer holds the money and owns the ledger.
  • Writes the conditional update for debit and explains why it avoids races without locks in code.
  • Uses clearing accounts and a saga for cross-shard transfers and shows they net to zero.
  • Mentions the escrow-equals-liabilities invariant and daily checks.
  • Handles hot merchants with sub-accounts or batched credits.
  • Models cashback as expiring lots with campaign budgets.

12 Mistakes

  • Reading the balance, checking in application code, then writing, allowing double spend.
  • Only a balance column with no ledger.
  • Crediting the wallet when the user returns from the payment page instead of on confirmed payment.
  • Using floats for money.
  • Ignoring KYC limits and regulatory rules.
  • Using a distributed lock around every transfer.

13 Follow-ups

Q1How would you add interest-free 'pay later' on top of the wallet?
Q2How do you migrate wallets to a new shard layout without downtime?
Q3How would you support wallet-to-UPI interoperability?
Q4How do you detect cashback abuse across many fake accounts?
Q5What is your plan if the ledger shows a mismatch with the escrow bank statement?
Q6How would you give users a real-time balance on a smartwatch?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free