Javathoughts Logo
Javathoughts
Published on
Views

Design a Payment System System Design Interview Guide

Authors
  • avatar
    Name
    Javed Shaikh
    Twitter

← 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 succeeded or failed
  • 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

RequirementWhat it means
Initiate paymentCreate a payment with amount, currency, method, and order id.
Provider integrationTalk to Stripe-like / bank APIs. Tokenized methods only.
IdempotencySame key + same body = same payment. No double charge.
Payment statescreated → pending → succeeded / failed / canceled.
WebhooksProvider callbacks update status when the HTTP call returns early.
RefundsFull or partial, also idempotent.
ReconciliationNightly match: provider report vs your ledger.
Fraud / risk checksVelocity, amount, device, country. Fail closed or step-up.
LedgerAppend-only credits and debits. Balance is derived, not a loose integer.
Receipts / notifyEmail 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

RequirementWhy it matters
Exactly-once business effectUsers retry. Networks retry. Charge once.
AuditabilityFinance and disputes need history, not overwritten rows.
Availability of initiateCheckout cannot freeze. Prefer async confirm if the provider is slow.
SecurityPCI: use hosted fields / tokens. Encrypt secrets. Least privilege.
DurabilityLedger rows must survive a crash.
LatencyInitiate 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

FieldNotes
payment_idInternal id
idempotency_keyUnique with customer_id or merchant_id
order_id
amount_cents, currencyInteger money. No floats.
statusState 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

Payment System architectureCheckout hits the payment service. The provider charge is separate from your ledger write. Webhooks and a queue finish status, notifications, and reconciliation.chargecallbackasync events👤User🌐API Gateway⚙️Payment Service💳Payment Provider🗄️Payment DB🧾Ledger📩Webhook📩Event Queue👷Reconciliation Worker🔔Notification
Checkout hits the payment service. The provider charge is separate from your ledger write. Webhooks and a queue finish status, notifications, and reconciliation.

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:

  1. Validate amount, currency, token, idempotency key.
  2. Risk check (sync, cheap rules). Hard fraud → reject.
  3. Insert payments as pending.
  4. Call provider. If they return success immediately, mark succeeded and write ledger.
  5. If they return processing, wait for webhook.
  6. 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

BottleneckWhat you do
Provider latencyTimeouts, async confirm, do not hold DB transactions open during HTTP
Webhook stormsDedup on event id, bounded workers
Hot merchantPartition payments by merchant or payment_id
Ledger writesBatch, sequential per account if needed
Reconciliation scansSeparate warehouse, not OLTP

10. Tradeoffs

ChoiceUpsideDownside
Sync provider callSimple UXTimeouts, double-click risk without idempotency
Auth then captureCancel before shipExtra states
Immediate captureSimpleHarder refunds vs voids
SQL ledgerEasy constraintsScale writes
Event-sourced ledgerPerfect historyHarder queries

Pick: SQL payments + append-only ledger, idempotency keys, verified webhooks, async notify.


11. Failure Modes

FailureHandling
Timeout after provider chargedIdempotent retrieve by key / provider id. Reconciliation job
Webhook never arrivesPoll provider after 30s / 5m
Duplicate webhookUnique provider_event_id
Partial refund raceSerialize refunds per payment_id
Risk service downFail open for tiny amounts or fail closed — say which and why
Queue downPayment 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.


Related JavaThoughts reading:


Next in this series: Design E-commerce Cart, Order, and Inventory.