SkillPilotAI

Case Study 9: Payment Gateway like Razorpay

A platform for merchants that accepts money by UPI, cards and netbanking, records it safely in a ledger and settles it to the merchant.

C.1 Requirements

Functional requirements:

  • Merchant onboarding, API keys and webhook URL configuration.
  • Create orders and show payment methods at checkout (UPI, cards, netbanking, wallets).
  • Authorize/capture payments through the bank, card network or NPCI.
  • Idempotency on every API: no double charge on a retry.
  • Send webhooks for payment events to the merchant (payment.captured, refund.processed).
  • Account for every rupee in a double-entry ledger.
  • Merchant settlement (T+1/T+2) and reconciliation with bank files.
  • Full and partial refunds.

Non-functional requirements:

  • 99.99% availability of the payment API: downtime = the merchant's revenue loss.
  • Payment create API p99 < 300 ms (bank latency is separate).
  • Strong consistency in the ledger: debit = credit always, money must never vanish.
  • PCI-DSS compliance: card data tokenized and encrypted, the vault isolated.
  • Peak 10,000 TPS (sale days such as Big Billion Days).
  • Data durability: retain financial records for 8-10 years (regulatory).

C.2 Estimation (back-of-the-envelope)

ItemNumberHow it is derived
Transactions per day~2 croreAssumption for a large Indian gateway.
Average TPS~2302 crore / 86,400.
Peak TPS~10,000Festival sales and the 8-11 PM peak are ~40x the average.
Ledger entries per day~8 croreEach payment makes ~4 entries (customer, merchant, fees, GST).
Storage per day~60 GB2 crore payments x ~1 KB + 8 crore ledger rows x ~500 bytes.
Storage per year~22 TB60 GB x 365, replication extra.
Webhook deliveries per day~5 crore2-3 events per payment (authorized, captured, etc.).

C.3 API design

EndpointPurpose
POST /v1/ordersCreate an order with amount, currency, receipt (Idempotency-Key)
POST /v1/paymentsInitiate a payment from checkout: with method and order_id
POST /v1/payments/{id}/captureCapture an authorized payment
GET /v1/payments/{id}Fetch the payment status
POST /v1/payments/{id}/refundsFull or partial refund (Idempotency-Key)
POST /internal/callbacks/{bank}Async callback from the bank / NPCI / card network
GET /v1/settlementsThe merchant's settlements and their breakup

C.4 Data model

Table / StoreFieldsWhy
payments (PostgreSQL/MySQL, sharded by merchant_id)payment_id PK, order_id, merchant_id, amount, method, status, gateway_ref, created_atACID state transitions; sharded by merchant_id for merchant-wise queries.
idempotency_keys (Redis + DB)merchant_id + key PK, request_hash, response, status, expires_atReturn the same response for the same key; error on a request_hash mismatch.
ledger_entries (RDBMS, append-only)entry_id, txn_id, account_id, debit, credit, currency, created_atDouble-entry: sum(debit) = sum(credit) for every txn, never updated/deleted.
refunds (RDBMS)refund_id, payment_id, amount, status, bank_ref, created_atTrack multiple partial refunds of one payment.
webhook_deliveries (Cassandra)event_id, merchant_id, payload, attempt_no, status, next_retry_atHigh write volume, retry tracking.
card_vault (isolated, HSM-backed)token, encrypted_pan, expiry, fingerprintThe PCI scope stays limited to this small vault.

C.5 High-level architecture

Payment Gateway: architecture and request flow
  1. 1.Clients: Customer Checkout (UPI / card / netbanking, checkout JS from CDN) and Merchant Server (API key + secret)
  2. 2.WAF + DDoS -> Load Balancer -> API Gateway (auth, per-merchant rate limit)
  3. 3.Services: Order Service (POST /orders, Redis idempotency keys), Payment Service (state machine + idempotency), Refund Service (refund <= captured), Card Vault (token + HSM, PCI scope only here)
  4. 4.Payment Router (success rate + cost) -> Bank / NPCI / Card adapters -> NPCI / Bank (customer PIN, debit); the async callback comes back
  5. 5.PostgreSQL sharded by merchant_id (payments, refunds, outbox) -> outbox -> Kafka (payment.captured, refund.processed)
  6. 6.Consumers: Ledger Writer (double-entry rows) -> Ledger DB (append-only, debit = credit, 8 crore rows / day); Webhook Dispatcher (HMAC sign, backoff) -> merchant webhook, Cassandra webhook_deliveries; Settlement Engine (T+1 / T+2 payout) -> merchant bank a/c
  7. 7.Reconciliation Job: matches the bank / NPCI settlement files every night

Scale: 2 crore txn/day, peak 10k TPS, p99 < 300 ms.

  • Customer checkout comes via the CDN and the merchant server comes with an API key; both pass through WAF + DDoS -> Load Balancer -> API Gateway (auth, per-merchant rate limit).
  • Order / Payment / Refund services; the card number lives only in the isolated Card Vault (token + HSM), so the PCI scope is small.
  • Payment Service runs the state machine + Idempotency Redis; the Payment Router picks a route by success rate and cost, and adapters talk to NPCI / the bank.
  • Status goes into the PostgreSQL merchant_id shard + outbox -> Kafka; Ledger Writer, Webhook Dispatcher, Settlement and Reconciliation run async.
ComponentWhat it does
API GatewayAuth with the merchant API key/secret, per-merchant rate limit, request logging.
Payment ServiceRuns the payment state machine: created -> authorized -> captured / failed. Idempotency is enforced here.
Payment RouterChooses the best acquirer/PSP by method, bank success rate and cost; a fallback route on failure.
Card VaultCard numbers are tokenized and HSM-encrypted; the other services see only the token.
Ledger WriterConsumes Kafka events and writes double-entry rows; append-only, immutable.
Webhook DispatcherSends HMAC-signed webhooks to the merchant, retrying with exponential backoff for a few days.
Reconciliation JobMatches bank/NPCI settlement files against internal records; an alert or auto-correction on a mismatch.

C.6 Main flows

UPI payment flow:

  1. The merchant server does POST /orders with an Idempotency-Key and gets an order_id.
  2. The customer chooses UPI at checkout; POST /payments, status=CREATED.
  3. The Payment Router sends a collect request or intent to NPCI via the PSP bank.
  4. The customer enters the PIN in their UPI app; the bank debits.
  5. An async callback arrives from NPCI/PSP; verify the signature and dedup on gateway_ref.
  6. Payment status becomes CAPTURED (in a DB transaction) and a payment.captured event goes on Kafka.
  7. The Ledger Writer writes the entries: customer clearing debit, merchant payable credit, fee credit.
  8. The Webhook Dispatcher sends a signed webhook to the merchant; the merchant confirms the order.

Idempotent retry flow:

  1. The merchant's network times out and they retry the request with the same Idempotency-Key.
  2. The Payment Service looks up the key in Redis/DB.
  3. If the key is IN_PROGRESS, 409 or wait; if COMPLETED, return the stored response.
  4. If request_hash differs, a 422 error: the same key with a different body is not allowed.
  5. This way the customer's money is deducted only once.

Refund and reconciliation flow:

  1. The merchant does POST /payments/{id}/refunds with an amount.
  2. The Refund Service checks: total refunded <= captured amount.
  3. Reverse entries in the ledger; the amount is held from the merchant balance.
  4. A refund request to the bank/NPCI; an async status callback.
  5. At night the Reconciliation Job matches every txn against the bank file.
  6. On a mismatch (for example success at the bank, failed with us), an auto-update or an ops ticket.

C.7 Deep dives

UPI payment sequence (Rs 1000 order)
  1. 1.1. Merchant -> POST /orders + Idem-Key -> order_id
  2. 2.2. Customer / UPI app -> POST /payments UPI -> CREATED
  3. 3.3. Payment Service: Router -> PSP: collect / intent
  4. 4.4. PSP / NPCI / Bank: UPI PIN -> bank debit
  5. 5.5. Callback: sign verify + dedup
  6. 6.6. CAPTURED + outbox in one DB txn
  7. 7.7. payment.captured -> Ledger
  8. 8.8. HMAC signed webhook -> merchant confirms the order
Idempotency-Key lifecycle
  1. 1.Merchant timeout -> retry with the same Idempotency-Key
  2. 2.Lookup in Redis / DB: merchant_id + key + request_hash
  3. 3.New key: save IN_PROGRESS -> process -> COMPLETED
  4. 4.IN_PROGRESS: 409 or wait; COMPLETED: return the stored response
  5. 5.request_hash differs: 422 error

A DB unique constraint is the final guard: money is deducted only once.

Double-entry ledger (a Rs 1000 payment: the merchant's share, fee and GST):

AccountDebitCredit
customer clearing1000.00-
merchant payable-976.40
gateway fee-20.00
GST on fee-3.60
sum1000.001000.00

The ledger is append-only: a mistake is fixed with a reversal entry, never an update / delete. Refund = reverse entries + a merchant balance hold. At night the Recon Job matches the bank / NPCI settlement file against payments + ledger: match OK, or on a mismatch an auto-fix / ticket.

Problem: double charge on retry / network timeout. A merchant-scoped Idempotency-Key on every write API, stored with the request hash. A DB unique constraint is the final guard. Bank callbacks are also deduped on gateway_ref.

Problem: the payment state is unknown (bank timeout). Keep the status PENDING, poll the bank status-check API, and treat the reconciliation file as the final truth. Show the customer pending, not a wrong failure.

Problem: ledger correctness. A double-entry, append-only ledger; sum(debit) = sum(credit) is checked inside the DB transaction for every transaction. A mistake is fixed with a reversal entry, never an update.

Problem: webhook delivery reliability. The outbox pattern: the payment update and the event insert in one DB transaction, then Kafka. Retries with backoff, an HMAC signature, and merchant-side dedup by event_id.

Problem: bank/acquirer downtime. The Payment Router tracks every bank's real-time success rate; a circuit breaker shuts the bad route and moves traffic to another acquirer.

Problem: PCI-DSS and security. Card data only in an isolated vault, with tokenization + HSM encryption. Network segmentation, TLS everywhere, secrets rotation, audit logs.

C.8 Trade-offs

DecisionWhat was chosen and why
SQL vs NoSQL for payments/ledgerSQL (PostgreSQL/MySQL): ACID and strong consistency are non-negotiable for money. Cassandra for logs/webhooks.
Sync ledger write vs async via KafkaPayment status is written sync in the DB, the ledger async via outbox + Kafka: low latency, though the ledger may lag a little, and reconciliation covers that.
Single acquirer vs smart routingSmart routing: more complexity, but both success rate and cost are better.
Auto-capture vs manual captureAuto-capture by default; a manual option for merchants who want to capture after delivery.

C.9 Interview tips

  • Draw the payment as a state machine, with every transition idempotent.
  • Explain the full Idempotency-Key lifecycle: store, in-progress, completed, hash mismatch.
  • Explain why the ledger is double-entry and append-only: this is a senior-level signal.
  • Call reconciliation the final safety net: mismatches are bound to happen in distributed systems.
  • Be sure to mention the outbox pattern + retry + signature for webhooks.