All articles
System DesignPaymentsHLDLLD28 min read

How UPI Payments Actually Work — NPCI, System Design & the Interview Playbook

A deep dive into the UPI payment flow, NPCI's role as the central switch, the full high-level and low-level design, and exactly how to design UPI step by step in a system design interview.

UPI processes more transactions than Visa and Mastercard do in India combined — north of 13 billion a month, peaking around 7,000 transactions per second, with an end-to-end latency budget measured in single-digit seconds and a correctness requirement of "never lose a rupee." It is, without exaggeration, one of the best real-world system design case studies available, and it shows up constantly in backend and platform interviews.

This post walks the whole thing: what happens between tapping "Pay" and the receiver's phone buzzing, what NPCI is really doing in the middle, the architecture you'd draw on a whiteboard, and a step-by-step interview script that starts from a naive design and evolves into something defensible.

1. What UPI actually is

UPI (Unified Payments Interface) is not a wallet and it does not hold your money. It is an interoperability layer and messaging standard operated by NPCI that lets any app talk to any bank account using a single addressing scheme — the VPA (Virtual Payment Address, e.g. upasana@okhdfc).

Three ideas make it work:

  • Abstraction of the account. A VPA maps to an account number + IFSC inside the bank. You share the VPA; the account details never leave the bank.
  • Push and pull in one rail. Both "send money" (push credit) and "request money" / "collect" (pull debit with payer authorization) run on the same message set.
  • Authorization at the issuer, not the app. The UPI PIN is captured by a hardware-isolated NPCI Common Library inside the app, encrypted on the device, and can only be decrypted by the issuing bank's HSM. Google Pay never sees your PIN.

2. The players

EntityRole
Payer / PayeeEnd users, or a merchant on the payee side (P2M).
PSP app (TPAP)The UI: GPay, PhonePe, Paytm, BHIM. Third-party apps are TPAPs and must ride on a sponsor bank's PSP handle.
PSP bank (sponsor bank)The regulated bank that owns the UPI handle (@okaxis, @ybl, @paytm) and is the member that actually connects to NPCI.
Remitter / Issuer bankPayer's bank. Owns the balance, performs the debit, and validates the UPI PIN in its HSM.
Beneficiary bankPayee's bank. Performs the credit.
NPCI UPI switchThe central router, orchestrator, ledger of record for the interbank leg, and settlement engine.
Acquirer / merchant PSPFor P2M: onboards merchants, issues merchant VPAs and QR codes, does reconciliation and payouts.
Interview signal
Candidates who say "the app debits the account" lose points instantly. The app only composes a signed request; only the issuer bank moves money, and only NPCI can orchestrate the two-legged interbank transaction.

3. NPCI in depth

NPCI (National Payments Corporation of India) is an RBI-promoted, not-for-profit umbrella organisation for retail payments. It operates IMPS, RuPay, NACH, AePS, FASTag, BBPS and UPI. For UPI specifically it wears five different hats, and a strong answer separates them.

3.1 The switch (routing)

Every UPI message hits the NPCI switch. It resolves the VPA's handle suffix to the PSP bank, resolves the account to the issuer, and routes the ISO-8583-derived / XML-over-HTTPS API messages (ReqPay, RespPay, ReqAuthDetails, ReqChkTxn, ReqValAdd…) between four parties. It assigns the globally unique txnId (UPI transaction ID) that everyone reconciles against.

3.2 The orchestrator (2-phase money movement)

The interbank transfer is a distributed transaction across two independent banks that cannot hold a lock for each other. NPCI runs it as a saga: debit first, then credit, then compensate (auto-reversal) if the credit fails. NPCI keeps the state machine, the timers, and the retry/reversal obligations.

3.3 The clearing & settlement engine

Real-time to the user is not real-time between banks. During the day NPCI only moves messages; the actual money moves in net settlement cycles (UPI runs multiple cycles per day) through banks' settlement accounts at the RBI. NPCI nets each member's obligations — Bank A owes Bank B ₹412 crore — and posts a single settlement instruction. This is why a "successful" payment can still be reversed on T+0/T+1 after recon.

3.4 The rulebook & risk layer

  • Circulars defining message formats, timeouts, and TAT (turnaround time) obligations.
  • Limits: ₹1 lakh per transaction / 20 transactions per day by default, with higher caps for specific categories (capital markets, insurance, verified merchants).
  • Mandatory auto-reversal within T+1 for failed credits, and penalty regimes for TAT breaches.
  • Fraud analytics, velocity checks, blacklisted VPAs/devices, and the new-payee 24-hour cooling limit.
  • Market-share cap policy for TPAPs (the 30% rule) — an interesting non-technical constraint that has real routing consequences.

3.5 The certification authority

NPCI certifies PSPs and banks, runs the UPI Common Library (the shared PIN-capture component), manages the PKI that binds device → app → issuer, and audits members.

4. The end-to-end payment flow

Here is the canonical P2P "scan and pay" path.

Payer app        Payer PSP bank        NPCI switch        Remitter bank      Beneficiary bank
    |                   |                    |                   |                  |
 1. scan QR / enter VPA |                    |                   |                  |
    |---- ReqValAdd (validate payee VPA) --->|                   |                  |
    |<--- RespValAdd (name, verified flag) --|                   |                  |
 2. show payee name + amount, capture UPI PIN
    (PIN encrypted inside NPCI Common Library, keyed to issuer HSM)
    |---- ReqPay(txnId, payerVPA, payeeVPA, amt, encPIN, deviceFP) ------------------>|
    |                   |                    |                   |                  |
 3.                     |                    |--- ReqAuthDetails/Debit ------------->|
    |                   |                    |    (PIN verify in HSM, risk checks,   |
    |                   |                    |     balance check, DEBIT + hold)      |
    |                   |                    |<-- Debit SUCCESS ---------------------|
 4.                     |                    |------------- Credit request --------->|
    |                   |                    |<------------ Credit SUCCESS ----------|
 5.                     |<-- RespPay SUCCESS |                   |                  |
    |<-- push notify ---|                    |                   |                  |
                                             |--- notify payee PSP -----------------> payee app
 6. End of day: NPCI nets obligations -> RBI settlement account movement
UPI push payment — happy path

Step by step, with the detail that matters

  1. Address resolution (ReqValAdd). Before any money is discussed, the payee VPA is validated and the registered name is returned so the payer can visually confirm. This is also the anti-phishing control for QR codes.
  2. PIN capture. The UPI PIN is entered in the NPCI Common Library, not the PSP's own UI. It is encrypted with the issuer's public key along with a device fingerprint, app ID, and a random salt so the ciphertext is never replayable.
  3. ReqPay. The PSP bank signs and forwards the request. NPCI mints/records txnId, does its own risk scoring, and enters the transaction into its state machine as INITIATED.
  4. Debit leg. The remitter bank decrypts the PIN in an HSM, runs its own fraud rules and limits, checks balance, and posts the debit inside a local ACID transaction along with an idempotency record keyed on txnId.
  5. Credit leg. Only after a confirmed debit does NPCI ask the beneficiary bank to credit. If the credit fails or times out, NPCI triggers an auto-reversal back to the remitter (must complete within T+1 by regulation).
  6. Responses & notifications. Both PSPs are notified; both apps push notifications. If the payer app never received the response, it polls ReqChkTxn — which is why "payment status pending" resolves on its own.
  7. Settlement. Netting + RBI settlement, then reconciliation files exchanged with every member. Discrepancies produce chargebacks/adjustments through the URCS dispute system.
The single most important property
UPI is debit-first with compensating reversal, not two-phase commit. No bank will hold a prepare-lock on behalf of another bank across the internet. Say this sentence in an interview and you've already separated yourself from most candidates.

5. Registration, device binding and the UPI PIN

  1. User installs the PSP app and enters a mobile number.
  2. The app sends an outbound SMS from the device to a shortcode. This proves the SIM physically lives in that handset — this is the device-binding step, and it is why UPI registration fails on dual-SIM/eSIM edge cases and on tablets.
  3. NPCI issues a device token bound to (mobile, device fingerprint, app).
  4. The app queries banks by mobile number to discover linked accounts (masked), and the user picks one; a VPA is created and mapped.
  5. UPI PIN is set using debit card last 6 digits + expiry (or Aadhaar OTP). The PIN is stored only as a verifier inside the issuer's HSM.

6. System design: high-level architecture

If you are asked to "design UPI," you are usually being asked to design the NPCI switch plus a PSP. Here's the target picture.

                 ┌───────────────┐
  Mobile apps ──▶│  API Gateway  │  mTLS, signature verify, rate limit, WAF
  (PSP/TPAP)     └──────┬────────┘
                        │
        ┌───────────────┼────────────────────────────┐
        ▼               ▼                            ▼
 ┌────────────┐  ┌─────────────┐             ┌───────────────┐
 │ VPA / Addr │  │  Payment    │             │  Mandate /    │
 │  Registry  │  │ Orchestrator│◀── Kafka ──▶│ Autopay Svc   │
 └─────┬──────┘  └──────┬──────┘             └───────────────┘
       │                │  (saga state machine, idempotent)
       │      ┌─────────┼──────────┬─────────────┬──────────────┐
       ▼      ▼         ▼          ▼             ▼              ▼
  ┌────────┐ ┌──────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
  │Routing │ │Risk /│ │ Ledger │ │ Bank      │ │Notifica- │ │ Recon &  │
  │ Svc    │ │Fraud │ │ Service│ │ Connector │ │tion Svc  │ │Settlement│
  └────────┘ └──────┘ └───┬────┘ │ (per-bank │ └──────────┘ └────┬─────┘
                          │      │  circuit  │                   │
                 ┌────────▼───┐  │  breaker) │            ┌──────▼──────┐
                 │ Txn store  │  └─────┬─────┘            │ Data lake / │
                 │ (sharded   │        │                  │ files, RBI  │
                 │  Postgres  │   Issuer & Beneficiary     │ settlement  │
                 │  by txnId) │        banks               └─────────────┘
                 └────────────┘
Target architecture for the UPI switch and PSP

Service responsibilities

  • API gateway / edge: mTLS with member banks, XML/JSON signature verification, replay protection (nonce + timestamp), per-member quotas, and shedding.
  • VPA registry: read-heavy, cache-friendly (Redis, ~99% hit rate), maps handle → PSP bank, VPA → account token. Strongly consistent writes, eventually consistent reads are acceptable except during creation.
  • Payment orchestrator: the heart. A durable saga/state machine per transaction with persisted state transitions, timers, and compensations. Stateless pods + state in the DB, so any pod can resume any transaction.
  • Bank connector: one logical adapter per member bank with its own bulkhead, circuit breaker, connection pool and timeout profile. A slow bank must never exhaust global threads — this is the classic UPI outage cause.
  • Ledger service: append-only, double-entry, immutable. Never UPDATE balance SET…; write entries and derive balances with snapshots.
  • Risk engine: synchronous cheap rules (velocity, limits, blacklists) in the hot path; expensive ML scoring async or on a shadow path.
  • Recon & settlement: batch pipeline that ingests bank files, matches on txnId + RRN, raises exceptions, and computes net positions.

Data model choices

  • Transactions: sharded relational store (Postgres/Vitess/Spanner-like) sharded by txnId hash. Each transaction's whole lifecycle lives in one shard, so no cross-shard transaction is ever needed.
  • Ledger entries: append-only, partitioned by date, archived to object storage after 90 days; queries served from a columnar warehouse.
  • Events: Kafka with txnId as the partition key → per-txn ordering for free.
  • Cache: Redis for VPA resolution, idempotency keys, and rate-limit counters.

7. Capacity estimation

MetricEstimateHow you get there
Monthly transactions~14 BPublished NPCI volumes
Daily average~465 M14 B / 30
Average TPS~5.4 K465 M / 86,400
Peak TPS~15–20 K3–4× average (festival/evening peaks)
Messages per transaction6–8ValAdd, ReqPay, debit, credit, resp, notify, status
Peak message rate~120 K msg/s20 K × 6
Row size~1 KB with metadataIDs, amounts, status, timestamps, signatures
Hot storage / day~500 GB465 M × 1 KB
Hot storage / year~180 TB (before replication)500 GB × 365
Latency budgetp99 < 3 s end-to-endNPCI hop ~200 ms; banks dominate
Do the math out loud, but keep it cheap
Interviewers want to see that you can size shards and connection pools from the numbers — e.g. 20K TPS × 8 messages × 50 ms service time ≈ 8,000 concurrent in-flight requests, so you need on the order of 2,000+ cores and per-bank pools sized independently. They do not want five minutes of arithmetic.

8. How to design UPI in an interview

The trick is to evolve the design. Start deliberately naive, then break your own design and fix it. Here's a script that fits a 45-minute loop.

Minute 0–5: scope and requirements

Say out loud what you're building and what you're not.

  • In scope: P2P and P2M push payments, VPA management, status/recon, reversals.
  • Out of scope (call it, then park it): mandates/autopay, UPI Lite, credit-line on UPI, international UPI, dispute UI.
  • Functional: register + bind device, create VPA, resolve VPA, pay, collect request, check status, history, notifications, reversal.
  • Non-functional: 20K peak TPS; p99 < 3 s; 99.99% availability; no money loss ever (durability > availability on the ledger); auditable for 10 years; PCI-DSS and RBI compliance; idempotency across all APIs.

Minute 5–10: Design v1 — the naive monolith

[App] → [Payment Service] → [Postgres: accounts table]
                 BEGIN;
                   UPDATE accounts SET bal = bal - 500 WHERE id = payer;
                   UPDATE accounts SET bal = bal + 500 WHERE id = payee;
                 COMMIT;

Then immediately attack it: this only works when both accounts live in one database. In UPI they live in different banks. There is no shared transaction. Also: single point of failure, no idempotency, mutable balances (no audit trail), and hot-row contention on popular merchants.

Minute 10–18: v2 — introduce the switch and the saga

  • Add NPCI as a central switch with a per-transaction state machine.
  • States: INITIATED → DEBIT_PENDING → DEBITED → CREDIT_PENDING → SUCCESS, with DEBIT_FAILED, CREDIT_FAILED → REVERSAL_PENDING → REVERSED, DEFERRED/TIMEOUT.
  • Persist every transition before making the downstream call, so a crash can be resumed from the log.
  • Make every external API idempotent on txnId; the bank stores the result of the first call and replays it.

Minute 18–24: v3 — correctness under failure

  • Timeouts are not failures. A debit timeout is "unknown". You must run a status-check (ReqChkTxn) loop before deciding to reverse — otherwise you double-debit or wrongly refund.
  • Reconciler as a background sweeper. A job scans transactions stuck in non-terminal states past their SLA and drives them to terminal states. This is your safety net and interviewers explicitly look for it.
  • Double-entry immutable ledger instead of mutable balances, with a daily trial-balance job asserting debits == credits.
  • Exactly-once effect, at-least-once delivery. Retries + idempotency keys, never "hope the network is fine."

Minute 24–32: v4 — scale and isolation

  • Shard the transaction store by txnId; keep one transaction on one shard.
  • Cache VPA resolution; it's 30–40% of all traffic and is read-mostly.
  • Per-bank bulkheads + circuit breakers. When SBI is slow, only the SBI pool degrades; everything else keeps flowing. Fail fast with a "bank unavailable" response rather than queueing.
  • Backpressure and load shedding at the gateway with priority tiers (P2M checkout > balance enquiry).
  • Hot merchant accounts: don't lock a single row per credit — write ledger entries and aggregate, or use per-merchant sub-accounts / batched credits.
  • Multi-region active-active with synchronous replication for the ledger quorum.

Minute 32–38: v5 — security, risk, and settlement

  • PIN never in application memory in plaintext; HSM-backed decryption at the issuer only.
  • mTLS + per-message digital signatures between all members; nonce-based replay protection.
  • Device binding, app attestation, and SIM-change detection.
  • Real-time rules (velocity, new-payee cap, amount limits) synchronous; ML scoring async.
  • Netting + settlement cycles into RBI accounts; recon files and a dispute/chargeback flow.
  • Tokenized PII, field-level encryption, and immutable audit logs.

Minute 38–45: the final picture

Redraw the architecture from section 6 and narrate the request path once, end to end, calling out where each failure mode is handled. Then invite the interviewer into the part you find most interesting — usually reversals or hot-merchant contention.

9. Low level design

The LLD round usually zooms into the payment orchestrator. Model it as a state machine with pluggable strategies, not as a pile of if-statements.

Core entities

enum TxnState {
  INITIATED, RISK_APPROVED, DEBIT_PENDING, DEBITED,
  CREDIT_PENDING, SUCCESS,
  DEBIT_FAILED, CREDIT_FAILED,
  REVERSAL_PENDING, REVERSED, DEFERRED
}

class Transaction {
  TxnId       txnId;          // globally unique, idempotency key
  String      rrn;            // 12-digit retrieval reference number
  Vpa         payer, payee;
  Money       amount;         // long minorUnits + Currency, NEVER double
  TxnType     type;           // P2P_PUSH, P2M_PUSH, COLLECT, MANDATE, REFUND
  TxnState    state;
  int         version;        // optimistic locking
  Instant     createdAt, updatedAt, expiresAt;
  Map<String,String> meta;    // deviceFp, appId, merchantId, mcc
}

class LedgerEntry {          // append-only, never updated
  UUID    entryId;
  TxnId   txnId;
  String  accountId;
  Money   amount;
  Side    side;              // DEBIT | CREDIT
  Instant postedAt;
}
Domain model

The state machine

interface TransitionHandler {
  TxnState handle(Transaction txn, Event event);
}

class StateMachine {
  Map<Pair<TxnState, EventType>, TransitionHandler> table;

  void apply(Transaction txn, Event e) {
    var handler = table.get(Pair.of(txn.state, e.type));
    if (handler == null) throw new IllegalTransition(txn.state, e.type);
    var next = handler.handle(txn, e);
    txnRepo.compareAndSwapState(txn.txnId, txn.state, next, txn.version); // optimistic
    outbox.publish(new StateChanged(txn.txnId, next));                    // same DB txn
  }
}
Transitions are data, not control flow

The transactional outbox is the detail that makes this correct: the state change and the event that announces it are written in one local DB transaction, and a relay publishes to Kafka afterwards. Without it you get "state changed but nobody was told" or "told twice, acted twice."

Idempotency

class IdempotencyGuard {
  Optional<Response> begin(String key) {
    // INSERT ... ON CONFLICT DO NOTHING returns 0 rows if already seen
    if (!store.tryInsert(key, IN_PROGRESS, ttl)) {
      var rec = store.get(key);
      if (rec.status == IN_PROGRESS) throw new ConcurrentRequest();  // 409, client retries
      return Optional.of(rec.response);                              // replay stored result
    }
    return Optional.empty();
  }
  void complete(String key, Response r) { store.put(key, DONE, r); }
}

Patterns worth naming

PatternWhere it's used
State / State machineTransaction lifecycle
Saga + compensating transactionDebit → credit → auto-reversal
StrategyP2P vs P2M vs collect vs mandate execution rules
Chain of responsibilityRisk rule pipeline (limits → velocity → blacklist → ML)
Adapter + BulkheadPer-bank connectors with isolated pools
Transactional outboxReliable event publication
Circuit breaker + Retry with jitterBank calls
ObserverNotifications, analytics, audit fan-out
FactoryBuilding bank-specific request payloads

Key APIs

POST /v1/vpa                 { handle }                  -> 201 { vpa }
GET  /v1/vpa/{vpa}/resolve                               -> 200 { name, verified }
POST /v1/payments            Idempotency-Key: <txnId>
     { payerVpa, payeeVpa, amountMinor, currency, encPin, deviceFp, remarks }
                                                         -> 202 { txnId, state }
GET  /v1/payments/{txnId}                                -> 200 { state, rrn, ts }
POST /v1/payments/{txnId}/reverse  { reason }            -> 202 { reversalId }
POST /v1/collect             { payeeVpa, payerVpa, amountMinor, expiryMins }

Note the 202: payment initiation is asynchronous by nature. The client polls or listens on a socket/push channel. Returning 200 SUCCESS synchronously is a design smell in a system whose downstream is two independent banks.

Concurrency rules

  • Optimistic locking (version column) on the transaction row; never a long-held pessimistic lock.
  • Money as integer minor units. A double in a payment system is an automatic fail.
  • Balance derived from the ledger with periodic snapshots; hot accounts get sharded sub-ledgers.
  • All timers (expiry, reversal SLA) persisted in a durable scheduler, not in-process.

10. Edge cases and failure modes

ScenarioCorrect handling
Debit succeeded, credit failedAuto-reversal to payer within T+1; reversal is itself a transaction with its own idempotency and retries.
Debit response timed out (unknown)Never assume failure. Poll ReqChkTxn with backoff; only reverse after the bank confirms no debit, or after the recon file settles it.
User taps Pay twiceSame idempotency key from the client → second call replays the first result. Client-side debounce is a UX nicety, not a correctness control.
Money debited, app shows failedState is pending, not failed. Sweeper resolves it; user sees 'pending' with an auto-refund SLA — the single biggest real-world UPI support issue.
Beneficiary bank downFail fast via circuit breaker; do not debit at all if the target bank is known-down (pre-flight health).
Insufficient balanceClean terminal failure at the debit leg; no reversal needed.
Wrong VPA / correct VPA, wrong personNot a system failure — this is why ReqValAdd shows the registered name before PIN entry. Dispute flow, not reversal.
Duplicate credit in reconDetected by trial balance and RRN matching; adjustment entry posted, never a silent UPDATE.
Replay attackNonce + timestamp window + signature; PIN ciphertext is salted per request.
Peak-hour overloadPriority-based load shedding; queue non-critical (history, notifications) and protect the payment path.
Clock skew between membersAll state timestamps server-assigned at NPCI; use monotonic sequence for ordering, not wall clock.
Partial network partitionLedger quorum writes; if quorum is lost, refuse new debits (choose consistency over availability for money).

11. Follow-up questions to expect

  • How do you guarantee exactly-once money movement over an at-least-once network? Idempotency keys + terminal-state persistence + reconciliation. Exactly-once delivery is impossible; exactly-once effect is what you build.
  • Why not 2PC? Cross-organisational locks, unbounded blocking on coordinator failure, and no bank will grant another bank a prepare-lock. Saga + compensation with a regulated reversal SLA.
  • How does settlement differ from the transaction? Transactions are messages in real time; settlement is netted movement between bank accounts at RBI in discrete cycles.
  • How would you add UPI Autopay (mandates)? A mandate object with amount cap, frequency, validity, and pre-debit notification 24h ahead; execution is a scheduled payment referencing the mandate ID with its own idempotency.
  • How would you support UPI Lite? An on-device prepaid balance that avoids the issuer round-trip for small-value payments; batched settlement to the bank. Trade-off: lower latency and less load, at the cost of on-device state and a top-up flow.
  • How do you shard when a merchant does 50K credits/minute? Sub-accounts / entry-level sharding + async aggregation; never one row per merchant balance.
  • How do you test this? Deterministic simulation of bank failures, chaos on connectors, property-based tests asserting ledger invariants, and a shadow-traffic environment.
  • What's your observability story? Per-bank success-rate SLOs (NPCI publishes these), state-transition dashboards, stuck-transaction alarms, and trial-balance alerts.
  • How would you extend to cross-border? FX quote service, sanctions/AML screening in the hot path, and settlement through a correspondent bank with different cut-off times.

Practice this, don't just read it

Reading a UPI breakdown feels productive; drawing it under time pressure is a different skill. Try it on Archtin: sketch the switch, defend the saga, and get the reversal path picked apart before an interviewer does it for you.

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free