- Published on
- Views
Design a Payment System System Design Interview Guide
- Authors

- Name
- Javed Shaikh
← System Design Interview Preparation
This guide walks through Design a Payment System the way you would in a backend or Java interview. The numbers are interview estimates. They help you show your thinking. They are not a production capacity plan.
1. Problem
A payment system takes money from a customer and records it so the business can fulfill an order, refund later, and prove what happened.
Example:
- Shopper clicks Pay
- Your service creates a payment intent with an idempotency key
- A payment provider (card network partner, UPI, wallet) authorizes the charge
- A webhook later says
succeededorfailed - You write a ledger entry, mark the order paid, and send a receipt
You are not a bank. You orchestrate payment providers, keep a ledger, and never store raw card numbers.
This is the money path behind food delivery and e-commerce checkout.
2. Functional Requirements / FR
| Requirement | What it means |
|---|---|
| Initiate payment | Create a payment with amount, currency, method, and order id. |
| Provider integration | Talk to Stripe-like / bank APIs. Tokenized methods only. |
| Idempotency | Same key + same body = same payment. No double charge. |
| Payment states | created → pending → succeeded / failed / canceled. |
| Webhooks | Provider callbacks update status when the HTTP call returns early. |
| Refunds | Full or partial, also idempotent. |
| Reconciliation | Nightly match: provider report vs your ledger. |
| Fraud / risk checks | Velocity, amount, device, country. Fail closed or step-up. |
| Ledger | Append-only credits and debits. Balance is derived, not a loose integer. |
| Receipts / notify | Email or push after success or refund. |
Out of scope: building a card network, storing PAN/CVV, crypto settlement, full KYC product.
3. Non-Functional Requirements / NFR
| Requirement | Why it matters |
|---|---|
| Exactly-once business effect | Users retry. Networks retry. Charge once. |
| Auditability | Finance and disputes need history, not overwritten rows. |
| Availability of initiate | Checkout cannot freeze. Prefer async confirm if the provider is slow. |
| Security | PCI: use hosted fields / tokens. Encrypt secrets. Least privilege. |
| Durability | Ledger rows must survive a crash. |
| Latency | Initiate in a few hundred ms. Webhook processing can lag seconds. |
Interview line: idempotency + ledger + webhooks. The provider is the source of money movement. Your DB is the source of your truth.
4. Back-of-the-Envelope Calculation
Say these assumptions out loud.
Traffic assumptions
- 10 million checkout attempts per day
- 70% succeed → 7 million successful payments
- Read/write: checkout writes are hot; later reads are receipts, admin, and reconciliation
- Refunds ≈ 2% of successes
QPS
10,000,000 requests/day / 86,400 seconds = around 116 QPS.
If peak is 5x, design for around 580 QPS of payment initiates.
Webhook traffic is similar order (one callback per attempt).
Refunds:
7,000,000 × 0.02 / 86,400 ≈ 1.6 QPS average
Storage
Payment row ≈ 1 KB (ids, amounts, status, provider refs, no card data)
10,000,000 × 1 KB ≈ 10 GB/day of payment rows
Ledger: 2–4 entries per payment (authorize, capture, fee) → another 20–40 GB/day if verbose. Interview: tens of GB/day. Keep hot months on SSD; archive old ledgers.
Cache / memory
Cache idempotency key → payment_id for 24 hours (Redis). 10M keys × 200 bytes ≈ 2 GB. Comfortable.
Servers
~600 peak QPS, each initiate does DB write + provider HTTP.
If one app box handles 100 concurrent outbound calls safely, start with 8–12 payment service instances plus workers. These are interview estimates, not exact production numbers.
5. APIs
POST /v1/payments
Idempotency-Key: <uuid>
{ "orderId", "amountCents", "currency", "methodToken", "customerId" }
GET /v1/payments/{paymentId}
POST /v1/payments/{paymentId}/refunds
Idempotency-Key: <uuid>
{ "amountCents", "reason" }
POST /v1/webhooks/providers/{provider}
raw body + signature header
GET /v1/payments/{paymentId}/ledger
Errors: 409 conflicting idempotency payload, 402 declined, 422 currency mismatch, 429 risk throttle.
Never accept raw PAN. Client talks to the provider SDK; you receive a token.
6. Data Model
payments
| Field | Notes |
|---|---|
payment_id | Internal id |
idempotency_key | Unique with customer_id or merchant_id |
order_id | |
amount_cents, currency | Integer money. No floats. |
status | State machine |
provider, provider_charge_id | |
risk_score | |
created_at, updated_at |
Unique: (merchant_id, idempotency_key).
refunds
refund_id, payment_id, amount_cents, status, idempotency_key, provider_refund_id.
ledger_entries
Append-only: entry_id, account (customer_receivable, cash_clearing, fees, refunds), amount_cents (signed), payment_id, created_at. Never update. Reverse with a new row.
webhook_events
Store raw payload, signature valid flag, processed flag. Unique on provider_event_id.
7. High-Level Design
The payment service owns state. The provider owns the card rails. Workers own slow work.
Payment System architecture
Components:
- User → API Gateway → Payment Service
- Payment Provider: authorize/capture/refund
- Webhook path back into Payment Service
- Payment DB + Ledger
- Queue → Notification and Reconciliation Worker
Happy path:
- Validate amount, currency, token, idempotency key.
- Risk check (sync, cheap rules). Hard fraud → reject.
- Insert
paymentsaspending. - Call provider. If they return success immediately, mark
succeededand write ledger. - If they return
processing, wait for webhook. - Emit event: receipt, order service “paid”.
Why a ledger? A single status column is not enough when you have partial captures, fees, and refunds. Finance reconstructs balances from entries.
8. Deep Dives
Idempotency key
Client sends Idempotency-Key. You store hash of the body.
- Same key, same body → return original payment
- Same key, different body →
409 - TTL 24h is a common interview number
Retries after timeouts must use the same key.
Payment states
Keep a small machine: created, pending, succeeded, failed, canceled, refunded / partially_refunded.
Ignore webhook events that move backwards (succeeded then old pending). Compare timestamps or event ids.
Webhooks
Verify HMAC signature. Return 200 quickly after persisting the event. Process asynchronously so provider retries do not stack on your business logic.
Refunds
Refund ≤ captured amount. New idempotency key. Ledger: debit cash_clearing, credit refunds payable. Provider call may be async too.
Reconciliation
Daily: download provider settlement file. Join on provider_charge_id. Mismatches go to a queue for humans. This catches “provider succeeded, our row still pending”.
Fraud / risk
Simple interview version: max amount, velocity (N charges / hour / customer), country vs BIN, blocklist. Heavy ML can be a side service. Do not block the happy path for 2 seconds if a score cache exists.
Security
- Tokens only
- TLS everywhere
- Secrets in a vault
- Audit who refunded
- Separate prod keys from test
Point to rate limiting on initiate and login-like risk APIs.
9. Bottlenecks
| Bottleneck | What you do |
|---|---|
| Provider latency | Timeouts, async confirm, do not hold DB transactions open during HTTP |
| Webhook storms | Dedup on event id, bounded workers |
| Hot merchant | Partition payments by merchant or payment_id |
| Ledger writes | Batch, sequential per account if needed |
| Reconciliation scans | Separate warehouse, not OLTP |
10. Tradeoffs
| Choice | Upside | Downside |
|---|---|---|
| Sync provider call | Simple UX | Timeouts, double-click risk without idempotency |
| Auth then capture | Cancel before ship | Extra states |
| Immediate capture | Simple | Harder refunds vs voids |
| SQL ledger | Easy constraints | Scale writes |
| Event-sourced ledger | Perfect history | Harder queries |
Pick: SQL payments + append-only ledger, idempotency keys, verified webhooks, async notify.
11. Failure Modes
| Failure | Handling |
|---|---|
| Timeout after provider charged | Idempotent retrieve by key / provider id. Reconciliation job |
| Webhook never arrives | Poll provider after 30s / 5m |
| Duplicate webhook | Unique provider_event_id |
| Partial refund race | Serialize refunds per payment_id |
| Risk service down | Fail open for tiny amounts or fail closed — say which and why |
| Queue down | Payment still succeeds; receipt lags |
Fail toward not capturing twice, and not marking paid without a provider id.
12. Interview Answer in 10 Minutes
"I would design payments as an orchestration layer, not as a card processor.
Assume 10 million attempts a day: 10,000,000 / 86,400 is about 116 QPS, about 600 QPS at 5× peak. Storage is tens of gigabytes a day. The hard part is idempotency and truth, not QPS.
The client sends an idempotency key. We insert a pending payment, run a cheap risk check, and call a tokenized provider. We never store raw cards. Status moves with webhooks that we signature-check and dedupe. A ledger is append-only so refunds and fees are extra rows, not edits.
A worker matches provider reports to our rows. Notifications go on a queue so checkout does not wait on email.
If the HTTP call times out, we look up by the same key. If webhooks lag, we poll. Double charge is the failure we refuse."
13. Interview Talking Points
- Idempotency key is mandatory.
- Integer cents, never floats.
- Ledger is append-only.
- Webhooks are untrusted until signed and deduped.
- Reconciliation is a feature, not a hope.
- Metrics: success rate, latency, webhook lag, mismatch count, refund time.
- Fits Java / Spring Boot services with Kafka for events.
14. Follow-up Questions
How do you prevent double charge?
Unique idempotency key, unique provider charge id, and retrieve-before-create on timeout.
Auth vs capture?
Auth holds funds. Capture after fulfillment. Void if the order dies. Refund after capture.
Multi-currency?
Store currency on every row. Convert only in a pricing service, not in the ledger mix.
PCI?
Hosted fields + tokens. Your logs must redact tokens if they are sensitive.
Exactly-once webhooks?
At-least-once delivery + idempotent handlers.
15. Internal Links
Related JavaThoughts reading:
- System Design Interview Preparation
- Design Food Delivery / Order Workflow
- Design E-commerce Cart, Order, and Inventory
- Design a Rate Limiter
- E-commerce platform system design
- Java
- Spring Boot
- Kafka
- Microservices
- Distributed Systems
- Event-driven architecture
- 20 system design concepts
Next in this series: Design E-commerce Cart, Order, and Inventory.
