Design problems
Payments & fintechHardSDE-2 40 min

Design UPI payments (PhonePe / Google Pay)

Asked at
AmazonCREDGooglePaytmPhonePeRazorpay

The interviewer asks

“Design a UPI payments app like PhonePe or Google Pay: users link their bank accounts, pay anyone by UPI ID, phone number or QR code, and see their history. Explain how your app, your partner PSP bank and NPCI fit together, and how you handle billions of transactions a month with tight timeouts and pending payments.”

What a strong answer looks like

A strong answer first draws the UPI ecosystem correctly: the app (a third-party app provider, TPAP) never holds or moves money; it talks to NPCI's UPI switch through one or more sponsor PSP banks, and NPCI orchestrates the debit at the payer's bank (which verifies the UPI PIN) and the credit at the payee's bank. The PIN is captured by NPCI's common library and encrypted end to end, so the app never sees it. So the app's design is about: secure onboarding (device binding via SMS from the SIM, account discovery, PIN setup), a fast pay path (VPA resolution, risk checks, a request to the PSP with a unique transaction ID), and above all handling outcomes that are pending or unknown, using callbacks, bounded status checks and reconciliation, never retrying a debit with a new ID. Around this: a transaction history store for billions of rows, notifications, fraud controls, and resilience across multiple PSP banks. Numbers: UPI does around 20 billion transactions a month; a leading app handles close to half of them, which is a few thousand per second on average and over 10k/s at peaks.

Concepts to mention

TPAP, PSP bank, NPCI, remitter and beneficiary banksdevice binding via SIM SMSUPI PIN encrypted by NPCI common libraryVPA resolution and handlesunique transaction ID and client idempotencypending / deemed transactions and status checksreversal and RBI TAT for failed debitsmultiple PSP banks for resilienceSQL hot store + Cassandra historyinline risk checks with fail-open rules

1 Requirements

Functional

  • Onboard a user: verify mobile number with device binding, discover bank accounts linked to it, set or reset UPI PIN.
  • Create a UPI ID (VPA) like name@handle; link multiple bank accounts.
  • Pay to a VPA, mobile number, bank account + IFSC, or by scanning a merchant QR (P2P and P2M).
  • Receive money and approve or decline collect requests.
  • Check bank balance; view transaction history with status (success, failed, pending).
  • Raise complaints for failed or pending transactions (debited but not credited).
  • Support UPI AutoPay mandates and UPI Lite for small payments.

Non-functional

  • Pay flow under ~3-5 s end to end in the normal case (most of it in NPCI and banks); app backend adds under 100 ms.
  • Respect NPCI timeouts and API rules (including limits on status checks).
  • 99.99% availability of the app backend; survive one PSP bank being degraded.
  • ~10k-15k transactions/s at peak (festival evenings, salary days, month start).
  • Zero tolerance for duplicate debits caused by the app; every transaction reaches a final state.
  • Payment data stored in India (RBI data localisation); strong security for device binding and PIN handling.

Out of scope (say it out loud)

  • Internals of NPCI's switch and bank core banking systems.
  • Merchant acquiring and settlement business (covered by the payment gateway design).
  • Credit on UPI and lending products.

2 Estimates

App transactions~9B/month ≈ 300M/day ≈ 3.5k/s avgUPI ≈ 20B/month nationally; a leading app with ~45% share ≈ 9B. 9B ÷ 30 ≈ 300M/day; ÷ 86,400 ≈ 3.5k/s.
Peak TPS~12k/sEvening and festival peaks ~3-4x average.
Requests per transaction~3-5 backend callsVPA resolve, pay request, callback, history write, notification; plus some status checks.
History storage~110B rows/year, ~40 TB/year300M/day × 365 ≈ 110B transactions/year; at ~200 bytes compressed ≈ 22 TB/year. Stored twice (payer and payee views) when both are our users: up to ~40 TB/year before replication.
Pending rate~0.5-1% of transactionsBank timeouts and deemed approvals; ~2-3M/day need status resolution.
Users~500M registered, ~200M monthly activeOrder of magnitude for a top UPI app in India.

3 API

EndpointWhat it does
POST/v1/onboarding/device-bindingStart binding: app sends an SMS with a token from the SIM to a PSP number; server confirms mobile + device.
GET/v1/accounts/discoverList bank accounts linked to the verified mobile number (via PSP → NPCI).
POST/v1/vpa/resolveValidate a VPA or mobile number and return the payee's verified name.
POST/v1/paymentsPay {payee, amount, note, encrypted PIN block from NPCI library, client_txn_id}; returns txn_id and status (success|failed|pending).
GET/v1/payments/{txn_id}Current status; triggers a status check with the PSP if pending and allowed.
GET/v1/history?cursor=User's transactions, newest first.
POST/psp/callbacksPSP bank callbacks for final status, incoming credits and collect requests; signed and verified.

4 Data model

users / devicesPostgres/MySQL sharded by user_id
user_idmobiledevice_iddevice_fingerprintsim_binding_statusrisk_flags

One active device binding per user; re-binding on new device or SIM change.

vpas / linked_accountsPostgres sharded by user_id
vpauser_idpsp_bankaccount_ref (masked)ifscis_default

The handle (e.g. @ybl, @ibl, @axl) determines the PSP bank used for the user's transactions.

transactionsSharded MySQL/Postgres (hot, last 30-90 days)
txn_id (unique, sent to NPCI)client_txn_id (unique per user)payer_vpapayee_vpaamount_paisestatusrrnerror_codepsp_bankcreated_atupdated_at

Status machine: initiated → pending → success|failed|deemed. Unique client_txn_id prevents duplicates from app retries.

historyCassandra / HBase
partition: user_id; clustering: created_at desctxn_id; payload (counterparty, amount, status)

Read-optimised per-user timeline; written from transaction events. Status updates overwrite the row by txn_id.

pending_queueKafka + delayed scheduler
txn_idnext_check_atchecks_done

Drives status checks within NPCI's allowed timing and count.

risk_featuresRedis / Aerospike
per userdevice and VPA: velocity countersnew-payee flagscomplaint history

Low-latency features for the pre-payment risk check (<20 ms).

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALencrypted PIN blockallow?txn rowReqPaydebit, creditcallbackTxnUpdatedpending txnsstatus checkUPI app + NPCI libraryAPI gatewayPayment servicePSP connector (switch)Risk / fraud serviceKafka (txn events)Pending resolverHistory + notifyworkersTxn DB (sharded)Cassandra (history)Redis (risk, VPAcache)PSP banksNPCI UPI switchPayer / payee banks
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

First-time onboarding

  1. 1User enters mobile number; app sends an SMS with a one-time token from the phone's SIM to the PSP bank's long code (device binding proves the SIM is in this phone).
  2. 2Backend receives the SMS via the PSP, matches token, mobile and device fingerprint, and marks the device bound.
  3. 3App calls account discovery; PSP asks NPCI, which queries banks for accounts linked to this mobile number.
  4. 4User picks an account; to set the UPI PIN, the NPCI common library collects debit card last 6 digits, expiry, the bank's OTP and the new PIN, encrypts them with NPCI/bank keys, and the request flows through the PSP to the issuing bank.
  5. 5A VPA like ravi@handle is created and mapped to the account at the PSP.

Pay ₹500 to a shopkeeper by QR

  1. 1App scans the QR, reads payee VPA, merchant name and code; it resolves the VPA to show the verified name.
  2. 2User enters the amount; the NPCI library shows the PIN screen and returns an encrypted PIN block. The app never sees the PIN.
  3. 3App sends POST /payments with a client_txn_id; payment service inserts the transaction (unique client_txn_id), runs risk checks (velocity, new payee, device risk) in under 20 ms.
  4. 4PSP connector sends ReqPay with our unique txn_id to the PSP bank, which forwards to NPCI.
  5. 5NPCI sends a debit request to the payer's bank (bank verifies PIN, debits) and then a credit request to the payee's bank, and returns the result with an RRN.
  6. 6PSP returns the final response synchronously or via callback; status moves to success, the event goes to Kafka, history and notifications update, and the merchant's soundbox announces the payment via the payee side.

Resolve a pending payment

  1. 1The PSP response times out or returns pending (e.g. payee bank did not respond).
  2. 2Transaction moves to pending; the user sees 'Payment pending, do not retry; we will update you'.
  3. 3Pending resolver schedules status checks per NPCI rules (limited count, spaced out) via the PSP.
  4. 4Final status arrives (success, failed or deemed success); if the payer was debited but the payment failed, the remitter bank reverses it, and the app shows reversal status.
  5. 5Unresolved cases at end of day are settled by NPCI/bank reconciliation reports; complaints raised in-app go through the UPI dispute system.

7 Deep dives

Who moves the money: the ecosystem

Many candidates design UPI as if the app debits accounts. It does not. A TPAP like PhonePe or Google Pay is a front-end; regulated PSP banks (e.g. Yes Bank, ICICI, Axis, SBI) sponsor it and connect to NPCI's UPI switch. NPCI routes a payment as a debit request to the payer's (remitter) bank and a credit request to the payee's (beneficiary) bank; banks hold the money and settle among themselves through NPCI's net settlement several times a day. The PIN is captured by NPCI's common library inside the app and encrypted so only the issuing bank can verify it; the TPAP and PSP cannot read it. The VPA handle (@ybl, @ibl, @axl, @okicici and so on) maps the user to a PSP bank. This shapes the design: our database is a record of requests and outcomes, not a ledger of balances; our critical job is to send each request exactly once with a unique transaction ID, track its outcome, and present it honestly. It also explains resilience: apps spread users across several PSP banks so one bank's outage does not stop all payments, and the PSP connector must handle each bank's API quirks and capacity limits.

Pending and deemed transactions

With hundreds of banks of varying quality, a fraction of a percent of transactions end without a clear answer in time: the payer bank debited but the payee bank timed out, or the response was lost. At 300M transactions a day, 0.5% is 1.5M users who see 'pending'. Rules: never treat a timeout as failure and never let the user retry with a new transaction ID while the first is pending, since that is how double debits happen; show clear status and guidance instead. Use the client_txn_id from the app so network retries of the same tap map to one transaction. Resolve pending transactions by callbacks first, then by status checks through the PSP; NPCI restricts how many status checks can be made and how soon, so the resolver uses a delayed queue with spaced attempts rather than tight polling. Some transactions end as 'deemed approved' when the beneficiary bank's credit is assumed done but confirmation is pending; these are settled through reconciliation. Debits on failed transactions are reversed by the remitter bank, and RBI's TAT framework sets deadlines and compensation for delayed reversals, so the app tracks those deadlines and escalates.

Scale and storage of history

Payments are short-lived requests, but history is forever and read constantly (users open history right after paying). Split the two. The hot transaction store is sharded MySQL/Postgres by txn_id or payer user_id, holding the last 30-90 days with strong consistency and unique constraints. Every status change is an event on Kafka, consumed to update a per-user history table in Cassandra or HBase, partitioned by user_id with time-ordered clustering, written once for the payer and once for the payee if they are our users. Reads of history are a single-partition query, which scales horizontally. Very active merchants have huge partitions, so bucket them by month (user_id + yyyymm). Older data moves to cheaper storage (S3 + Parquet) for analytics and statements. Notifications (push, SMS) also hang off the event stream so the payment path stays short. Capacity planning focuses on peaks: month start, festival evenings and events like Diwali, when TPS can triple in minutes; pre-scale and run load tests with PSP banks, since bank capacity is often the true limit.

Fraud and security

UPI fraud is mostly social engineering: fake collect requests ('enter PIN to receive money'), screen-sharing apps, fake customer care numbers, and SIM swap. Controls in the app: device binding tied to SIM, re-binding when SIM or device changes with cooling-off limits on amounts; clear warning screens on collect requests ('You are paying, not receiving'); detection of screen-sharing apps during PIN entry; velocity limits per user and per new payee; risk scoring with features like account age, payee's complaint history and unusual time or amount. The risk service runs inline with a tight latency budget (~20 ms) using precomputed features in Redis/Aerospike, and fails open with conservative rules rather than blocking all payments. Merchant VPAs flagged by many complaints can be blocked or shown with warnings. All payment data stays in Indian data centres per RBI rules.

8 Trade-offs

Multiple PSP banks vs one

One PSP bank is simpler to integrate and operate, but its outages take down all payments for the app. Several PSP banks cost more integration and reconciliation work but give resilience and negotiating power. Large apps use several handles and spread users.

Synchronous response vs callback-driven

Waiting synchronously for the PSP response gives a quick answer for most payments but holds connections for seconds. Callback-driven processing frees resources and handles slow banks better; most designs use a short synchronous wait, then switch to callback + status check, showing 'processing' meanwhile.

SQL for transactions, wide-column for history

Transactions need uniqueness and conditional status updates (SQL). History needs massive, cheap, per-user time-ordered reads (Cassandra/HBase). Using one store for both either loses guarantees or costs too much at 100B+ rows a year.

Fail open vs fail closed on risk checks

Failing closed blocks legitimate payments for millions when the risk service is slow. Failing open with simple rules (amount caps for new payees) keeps payments flowing while limiting exposure.

9 Bottlenecks

PSP bank or NPCI capacity at peak

Our backend can scale out; banks often cannot. Spread traffic across PSP banks where possible, pre-agree capacity, and shed non-payment load (balance checks, offers) first during peaks.

History partitions for big merchants

A busy merchant gets thousands of payments an hour. Bucket the partition key by month or day, and paginate.

Status-check storms

When a bank goes down, millions of transactions become pending and naive pollers flood the PSP. A delayed queue with spaced checks, per-bank rate limits and NPCI's limits prevents a retry storm.

10 Failure handling

A PSP bank is down

Users on that handle cannot pay via that bank; the app shows a clear message and, where the user has another linked handle or account, suggests it. New users are onboarded to healthy PSPs. Pending transactions resolve when the bank recovers.

Payer's bank debited but payee's bank failed

The transaction ends as failed or deemed; the remitter bank auto-reverses the debit. The app tracks the reversal, shows it to the user and escalates via the dispute system if the TAT deadline passes.

App retries a payment due to a network drop

The client_txn_id is unique per payment attempt, so the backend returns the existing transaction instead of sending a second ReqPay.

11 What impresses

  • Draws the ecosystem correctly and says the app never moves or holds money.
  • Knows the app never sees the PIN and explains device binding.
  • Designs the pending path carefully, with spaced status checks within NPCI limits and no blind retries.
  • Separates the hot transaction store from the history store with numbers for storage.
  • Mentions multiple PSP handles for resilience and bank capacity as the real peak limit.
  • Talks about social-engineering fraud and collect-request warnings, not just ML.

12 Mistakes

  • Designing a wallet-style ledger where the app debits and credits user balances.
  • Having the app or backend handle the raw PIN.
  • Marking timeouts as failed and inviting the user to pay again.
  • Polling status in a tight loop for pending transactions.
  • Ignoring data localisation and storing payment data outside India.
  • Storing years of history in the same sharded SQL tables used for live payments.

13 Follow-ups

Q1How would you implement UPI AutoPay mandates for monthly subscriptions?
Q2How does UPI Lite make small payments faster, and what changes in your design?
Q3How do you build a merchant soundbox that announces payments reliably?
Q4How would you migrate millions of users from one PSP handle to another?
Q5How do you detect a mule account receiving fraud money?
Q6What would you show on an operations dashboard during Diwali?

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