System design round

Design a UPI Payments App

A payments app holds no money: the banks do. Its backend records each payment, sends it to the UPI network, and waits for an answer that can take seconds or never come. The design is about that wait.

Estimate, draw your design, run its traffic through it and see which boxes break. Then answer the follow-up questions. The load test is free and needs no account; an AI review or score uses one of your AI replies.

The brief

It must

  • Send money to a UPI ID, see its status, and see past payments.
  • A payment is made once, however many times the app retries.
  • A payment with no reply from the bank is pending, never failed, until the network says otherwise.
  • Every change to a payment is recorded, for disputes and reconciliation.

The numbers

  • Assume a large payments app: 10,000 payments a second on a festival evening.
  • Users check a payment’s status or their history about three times per payment.
  • The UPI network, through your partner bank, takes about 1.5 seconds to answer and accepts up to 20,000 requests a second from you.
  • Each payment writes its row once and changes it twice more.

Before you draw: the numbers

Interviewers expect a quick estimate first, because the numbers decide the design. Write yours, then check.

The load test uses round numbers per box, the kind you would use on a whiteboard, so it shows where a design breaks and roughly by how much, not exact figures. How it works

The follow-up questions

Once your design is up, the interviewer goes deeper. Each question changes one thing and is a level further, Fresher to Senior. An AI interviewer scores your answer; 6 out of 10 or more climbs the rung.

UPI payments

Not climbed yet

Our design, and why

Try your own first. “Our design” on the board shows it as boxes you can load-test.

The bank takes about 1.5 seconds per payment, and at 10,000 payments a second that is 15,000 payments in flight at any moment. A server that holds each request open while it waits spends most of its capacity waiting. So the request writes the payment as PENDING, queues the bank call and returns; the app polls the status (or hears a push) until it settles.

  • Idempotency first: the app sends a request id with every attempt, and a unique constraint on (user_id, request_id) turns a double tap into a lookup of the first payment.
  • Bank callers behind Kafka send each payment once, with one transaction id reused on every retry. A timeout is unknown, not failed: they ask the network for the status with backoff and accept the callback whenever it comes.
  • The ledger takes about 30,000 writes a second, ten times what one SQL primary does, so it is sharded by payer. Entries are append-only and balances are derived from them.
  • Status reads (three per payment) come mostly from Redis, so polling does not compete with the writes.

When the bank goes silent, payments stay pending in the queue rather than failing, and the app says so plainly: "pending, do not pay again". A sweeper escalates anything pending too long, and end-of-day reconciliation catches what the callbacks missed.

What each level is expected to cover

The AI review grades your board against these.

Fresher
  • The app’s backend talks to the UPI network; banks hold the money.
  • A payments table with states INITIATED, PENDING, SUCCESS, FAILED.
SDE 1
  • Idempotency with a client request id and a unique constraint.
  • Money as integer paise; status history kept.
SDE 2
  • Timeouts treated as unknown: pending, status checks with backoff, callbacks handled idempotently.
  • Not holding request threads on a slow bank: queue the call, answer pending.
  • Sizing and sharding the ledger from the write rate.
Senior
  • A double-entry, append-only ledger with derived balances.
  • Reconciliation against settlement files, automatic reversals, audit trails.
  • Behaviour when the bank or network is down: what the user sees, what is retried, what is escalated.

How the load test works

Each round sends a few kinds of traffic from its users, at the rates in the brief. The test follows your arrows: a CDN answers what it can cache, a load balancer spreads requests, a service runs the logic and checks a cache beside it before the database, and a queue takes work off the request so the boxes after it work at their own pace.

Every box takes a rough number of requests a second (one service machine about 5,000, one Redis node about 100,000, one SQL primary about 3,000 writes, one row about 200 updates). A box asked for more turns red and drops the excess, unless a queue in front holds it back, in which case the queue grows and the work waits. Failure scenarios take machines away; a standby copy covers one.

It cannot see correctness: idempotency, consistency, and what users see when something fails. Write those in a box’s note; the AI review reads them.

blur