~learn.

India's UPI, the whole design.

A national switch that routes 800 million payments a day between 740+ banks in under 2 seconds, without ever holding the money.

6 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-01 · India's UPI · design sheethimanshukukreja.in/learn/sheets/bonus-01-indian-upi-system

At a glance#

Number Value What it drives
Payments a day ~800 million ~9,300 TPS average
Peak ~28,000–46,000 TPS 3–5× headroom for evenings and festivals
Backend ops at peak ~170,000+ / s each payment fans out: lookup, debit, credit, audit, 2 pushes
Users · banks 550M+ · 740+ one adapter and one circuit breaker per bank
End-to-end latency < 2 s p99 switch < 300 ms, each bank call < 700 ms
Storage ~900 GB / day · ~2.3 PB / 7 yr ~500 B per transaction plus audit logs
Availability 99.9 %, no maintenance window active-active data centres, RPO 0, RTO < 30 s

Architecture#

DiagramUPI on one page: apps → NPCI switch → banks
%% title: UPI on one page: apps → NPCI switch → banks
flowchart TD
  APP@{ icon: "sd:phone", label: "UPI apps · 550M+ users<br/>PhonePe · Google Pay · Paytm · BHIM" } --> PSP["PSP bank<br/>onboarding · VPA creation"]:::edge
  PSP ==>|"signed, encrypted"| LB@{ icon: "sd:load-balancer", label: "Health-checked LB<br/>2 active-active DCs" }
  LB --> TR["NPCI transaction router<br/>state machine · idempotency · fraud score"]:::hot
  TR --> VPA["VPA mapper"]:::service
  VPA --> VC@{ icon: "tech:redis", label: "Redis<br/>VPA + bank config" }
  TR ==> GW["Bank gateway<br/>adapter + breaker per bank · mTLS"]:::service
  GW ==>|"debit"| RB@{ icon: "sd:bank", label: "Remitter bank" }
  GW ==>|"credit"| BB@{ icon: "sd:bank", label: "Beneficiary bank" }
  TR --> EV@{ icon: "tech:kafka", label: "Kafka<br/>transaction events" }
  EV --> AU@{ icon: "sd:log", label: "Audit trail" }
  EV --> SET["Settlement engine<br/>net positions · cycles a day"]:::service
  SET --> RBI@{ icon: "sd:building", label: "RBI settlement" }
  class APP client
  class LB edge
  class VC cache
  class EV queue
  class AU,RBI store
  class RB,BB external
DiagramBank gateway: one adapter and one breaker per bank
%% title: Bank gateway: one adapter and one breaker per bank
flowchart TD
  TR["Transaction router"]:::hot --> AD["Bank adapter layer<br/>each bank's quirks · timeouts · retries"]:::service
  AD --> CB{"Per-bank circuit breaker"}
  CB -->|"CLOSED · healthy"| OK@{ icon: "sd:bank", label: "Healthy bank" }
  CB -->|"OPEN · skip 30 s, clear error"| DOWN@{ icon: "sd:bank", label: "Failing bank" }
  CB -->|"HALF-OPEN · probe"| PROBE@{ icon: "sd:bank", label: "Recovering bank" }
  AD -.-> SEC@{ icon: "sd:lock", label: "mTLS · request signing<br/>IP allow-list · leased lines" }
  class OK ok
  class DOWN bad
  class PROBE queue
  class SEC edge

Key flows#

DiagramPush payment: Alice pays Bob ₹500
%% title: Push payment: Alice pays Bob ₹500
sequenceDiagram
  autonumber
  participant A as Alice's app (PSP)
  participant N as NPCI switch
  participant H as Remitter bank (Alice)
  participant X as Beneficiary bank (Bob)
  A->>N: Pay ₹500 to bob@okaxis · txn ID + encrypted PIN
  Note over N: fraud score · idempotency check on txn ID
  N->>X: Resolve VPA bob@okaxis
  X-->>N: Bob's account
  N->>H: Debit ₹500 (verify PIN in bank HSM)
  H-->>N: Debited · ref ABC123
  N->>X: Credit ₹500
  X-->>N: Credited · ref XYZ789
  N-->>A: Success · TXN123456
  Note over A,X: under 2 s · audit row at every step · pushes to both sides
DiagramCredit times out: deemed success, then reconcile
%% title: Credit times out: deemed success, then reconcile
sequenceDiagram
  autonumber
  participant N as NPCI switch
  participant H as Remitter bank
  participant X as Beneficiary bank
  N->>H: Debit (txn ID T1)
  H-->>N: Debited
  N->>X: Credit (T1)
  X--xN: timeout
  Note over N: never re-send blindly: mark T1 DEEMED_SUCCESS
  N->>X: Status check (same T1, rate-limited)
  N->>H: T1 in the settlement file
  alt credit had landed
    X-->>N: credited → COMPLETED
  else credit never landed
    H-->>N: auto-reverse debit by T+1 → REVERSED
  end

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Transaction {
    +txnId : UUID · idempotency key
    +payerVpa : string
    +payeeVpa : string
    +amount : paise
    +state : TxnState
    +remitterRef : string
    +beneficiaryRef : string
    +createdAt : timestamp
  }
  class VPA {
    +address : string · bob@okaxis
    +bankCode : string
    +accountRef : token
    +deviceBinding : hash
  }
  class Bank {
    +code : string
    +adapter : string
    +breakerState : CLOSED / OPEN / HALF
    +timeoutMs : int
  }
  class SettlementBatch {
    +cycle : int
    +netPositions : paise per bank
    +status : string
  }
  class AuditEvent {
    +txnId : UUID
    +step : string
    +at : timestamp
  }
  Transaction "many" --> "1" VPA : payer / payee
  VPA "many" --> "1" Bank : held at
  Transaction "1" --> "many" AuditEvent : logged as
  SettlementBatch "1" --> "many" Transaction : nets

Lifecycle#

DiagramTransaction state machine
%% title: Transaction state machine
stateDiagram-v2
  [*] --> CREATED
  CREATED --> PENDING: validation passed
  PENDING --> DEBIT_INITIATED: send to remitter bank
  DEBIT_INITIATED --> FAILED: debit refused
  DEBIT_INITIATED --> DEBITED: debit ok
  DEBITED --> CREDIT_INITIATED: send to beneficiary bank
  CREDIT_INITIATED --> COMPLETED: credit ok
  CREDIT_INITIATED --> REVERSED: credit refused
  CREDIT_INITIATED --> DEEMED_SUCCESS: credit timeout
  DEEMED_SUCCESS --> COMPLETED: reconciled, credit landed
  DEEMED_SUCCESS --> REVERSED: auto-reverse by T+1
  COMPLETED --> [*]
  FAILED --> [*]
  REVERSED --> [*]
  class COMPLETED ok
  class FAILED,REVERSED bad
  class DEEMED_SUCCESS queue

Decisions#

Decision Chose Over Because
Who holds money Banks; the switch only routes A central wallet no counterparty risk, banks stay regulated, switch scales out
Atomicity across two banks State machine + one txn ID + reconciliation Two-phase commit across banks banks can't hold locks for you; a timeout must never become a double debit
Retries Same txn ID on every retry and status check New ID per attempt idempotent at the switch and the banks
Bank isolation Adapter + circuit breaker per bank One shared client one slow bank fails fast instead of draining the switch
Data centres Active-active, synchronous replication Active-passive RPO 0, RTO < 30 s, no maintenance window
Money in flight Real-time postings, net settlement in cycles Gross settlement per payment far fewer interbank transfers

When it breaks#

Failure What users see Contain it with
A big bank goes down (SBI ≈ a quarter of debits) "Bank temporarily unavailable", fast breaker opens, clear error, retry when it closes
Credit times out after debit "Pending", money back by T+1 at the latest deemed success, same-ID status checks, reconciliation
User taps Pay twice one debit txn ID created once on the device, checked at the switch
One NPCI data centre fails a blip active-active, health-checked routing, RPO 0
Status-check flood (12 Apr 2025: ~50 % success for ~2 h) payments fail nationwide rate-limit every API, separate status checks from the payment path

Say it in the interview#