Requirements & scope
Problem Statement & Requirements
Design a payment processing platform that handles the full lifecycle of financial transactions — accepting payments from customers, routing to card networks, managing settlements, handling refunds, and preventing fraud — similar to Stripe, Square, Adyen, or PayPal.
Functional Requirements
- Charge a payment method — credit/debit cards, bank transfers (ACH), digital wallets (Apple Pay, Google Pay)
- Payment intents — two-phase: authorize (hold funds) → capture (collect funds), or single-phase direct charge
- Refunds — full or partial refund of a completed payment
- Payment methods — store, retrieve, and delete customer payment methods (tokenized)
- Idempotency — every API call accepts an idempotency key; retries are safe and produce identical results
- Webhooks — notify merchants of async events (payment succeeded, refund completed, dispute opened)
- Multi-currency — accept payments in 135+ currencies; settle in merchant's preferred currency
- Subscriptions / Recurring billing — charge customers on a schedule (monthly, annual)
- Dispute / Chargeback handling — manage the lifecycle of customer-initiated disputes
- Payout / Settlement — aggregate funds and transfer to merchant bank accounts on a schedule
- Ledger — double-entry bookkeeping for every money movement; audit trail
Non-Functional Requirements
- Correctness above all — money must never be created, lost, or double-counted
- Exactly-once processing — no duplicate charges, no lost refunds
- High availability — 99.999% for payment acceptance (< 5 min downtime/year)
- Low latency — payment authorization in < 2 seconds end-to-end
- PCI DSS compliance — Level 1; card data encrypted at rest and in transit; tokenization
- Strong consistency — payment state transitions must be ACID
- Auditability — every state change logged immutably for regulatory compliance
- Scalability — handle Black Friday traffic (10x normal peak)
Out of Scope
- Detailed card network protocol internals (ISO 8583 message format)
- KYC/AML onboarding flow for merchants
- Tax calculation engine
- Full invoicing system
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| Payments processed / day | 50M |
| Payments / second (avg) | ~580 TPS |
| Payments / second (peak — Black Friday) | ~6,000 TPS |
| Refunds / day | 2.5M (5% refund rate) |
| Webhook deliveries / day | 200M (avg 4 events per payment lifecycle) |
| API calls / second (all endpoints) | ~10K RPS |
| Active merchants | 5M |
| Stored payment methods (tokens) | 2B |
Storage
| Metric | Value |
|---|---|
| Payment record size | ~2 KB (metadata, status, amounts, timestamps) |
| Payments per year | 50M × 365 = ~18.25B |
| Payment storage (1 year) | 18.25B × 2 KB = ~36.5 TB |
| Ledger entries (2 per payment + refunds + fees) | ~50B/year |
| Ledger storage (1 year) | 50B × 500 bytes = ~25 TB |
| Event log (webhooks, audits) | ~20 TB/year |
| Total storage (1 year) | ~80 TB |
| Retention | 7+ years (regulatory) |
Bandwidth
| Metric | Value |
|---|---|
| API ingress | 10K RPS × 2 KB = ~20 MB/s |
| Webhook egress | 200M/day × 1 KB = ~2.3 MB/s avg, 50 MB/s peak |
| Card network traffic | ~580 TPS × 1 KB = ~0.6 MB/s |
Hardware Estimate
| Component | Spec |
|---|---|
| API servers | 20-50 (stateless, auto-scaling) |
| Payment processing workers | 30-100 (orchestrate payment flow) |
| Database (primary) | 10-20 shards (PostgreSQL, SSD-backed) |
| Ledger database | 5-10 dedicated nodes (append-only, SSD) |
| Card network gateway | 10-20 (connection pooling to Visa/MC) |
| Webhook delivery fleet | 20-50 (async, retry-capable) |
| Redis (idempotency + rate limiting) | 3-6 nodes |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Component Breakdown
Component BreakdownExcalidraw diagram · editable shapes · reveal step by stepExplore
API & contracts
API Design
Create Payment Intent (Authorize)
POST /v1/payment_intents
Idempotency-Key: pi_req_abc123
Authorization: Bearer sk_live_...
Request:
{
"amount": 2999, // cents (avoid floating point!)
"currency": "usd",
"payment_method": "pm_card_visa_4242",
"capture_method": "manual", // "automatic" for single-phase
"description": "Order #1234",
"metadata": {"order_id": "ord-1234"},
"customer": "cus_abc123",
"receipt_email": "alice@example.com"
}
Response (200 OK):
{
"id": "pi_3MtwBwLkdIwHu7ix",
"object": "payment_intent",
"amount": 2999,
"currency": "usd",
"status": "requires_confirmation",
"payment_method": "pm_card_visa_4242",
"client_secret": "pi_3MtwBw_secret_YkS2...", // for frontend confirmation
"created": 1712150400
}Confirm Payment (Execute Authorization)
POST /v1/payment_intents/{id}/confirm
Idempotency-Key: pi_confirm_abc123
Response (200 OK):
{
"id": "pi_3MtwBwLkdIwHu7ix",
"status": "requires_capture", // if manual capture
// OR
"status": "succeeded", // if automatic capture
"charges": {
"data": [{
"id": "ch_abc123",
"amount": 2999,
"currency": "usd",
"status": "succeeded",
"authorization_code": "A12345",
"network_transaction_id": "NT98765",
"risk_score": 12,
"outcome": {
"network_status": "approved_by_network",
"risk_level": "normal"
}
}]
}
}Capture Payment
POST /v1/payment_intents/{id}/capture
Idempotency-Key: pi_capture_abc123
Request:
{
"amount_to_capture": 2999 // can be less than authorized (partial capture)
}
Response (200 OK):
{
"id": "pi_3MtwBwLkdIwHu7ix",
"status": "succeeded",
"amount_captured": 2999
}Create Refund
POST /v1/refunds
Idempotency-Key: ref_req_xyz789
Request:
{
"payment_intent": "pi_3MtwBwLkdIwHu7ix",
"amount": 1500, // partial refund (in cents)
"reason": "customer_request" // customer_request | duplicate | fraudulent
}
Response (200 OK):
{
"id": "re_abc123",
"amount": 1500,
"currency": "usd",
"status": "pending", // pending → succeeded (async from network)
"payment_intent": "pi_3MtwBwLkdIwHu7ix"
}Webhook Event
POST https://merchant.example.com/webhooks/stripe
Stripe-Signature: t=1712150400,v1=sha256_hmac_signature
{
"id": "evt_abc123",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_3MtwBwLkdIwHu7ix",
"amount": 2999,
"status": "succeeded"
}
},
"created": 1712150400
}
Merchant must respond 2xx within 10 seconds.
No 2xx → retry with exponential backoff (up to 3 days, ~20 attempts).Data model
Data Model
Payment Intent
Payment IntentExcalidraw diagram · editable shapes · reveal step by stepExplore
Ledger Entry (Double-Entry Bookkeeping)
Ledger Entry (Double-Entry Bookkeeping)Excalidraw diagram · editable shapes · reveal step by stepExplore
Payment State Machine
Payment State MachineExcalidraw diagram · editable shapes · reveal step by stepExplore
Core design decisions
Core Design Decisions
Decision 1: How to Guarantee Exactly-Once Payment Processing
This is the single most important design problem. Charging a customer twice is catastrophic.
Decision 1: How to Guarantee Exactly-Once Payment ProcessingExcalidraw diagram · editable shapes · reveal step by stepExplore
Solution: Multi-Layer Idempotency
Solution: Multi-Layer IdempotencyExcalidraw diagram · editable shapes · reveal step by stepExplore
Decision 2: The Double-Entry Ledger
Decision 2: The Double-Entry LedgerExcalidraw diagram · editable shapes · reveal step by stepExplore
Decision 3: Smart Routing to Maximize Authorization Rate
Decision 3: Smart Routing to Maximize Authorization RateExcalidraw diagram · editable shapes · reveal step by stepExplore
Request flows
Detailed Flow Diagrams
Payment Authorization Flow
Payment Authorization FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Refund Flow
Refund FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Settlement / Payout Flow
Settlement / Payout FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Workshop note · added for the website’s common reading format
Start with the dominant access pattern of Payment System. Ledger is one place to inspect capacity and tail latency. Measure before introducing a cache: define the cache key, invalidation policy, stale-data budget, and cold-start behavior.
Batch independent work where the latency budget allows it. Bound queues and concurrency, and verify that an optimization does not move the bottleneck to a dependency.
Advanced design
Tokenization & PCI Compliance
Tokenization & PCI ComplianceExcalidraw diagram · editable shapes · reveal step by stepExplore
Fraud Detection Engine
Fraud Detection EngineExcalidraw diagram · editable shapes · reveal step by stepExplore
Subscription / Recurring Billing
Subscription / Recurring BillingExcalidraw diagram · editable shapes · reveal step by stepExplore
Edge cases
Handling Edge Cases
Partial Capture / Over-Capture
Authorized: $100.00
Case 1: Partial capture (customer removed items from cart)
→ Capture $75.00. Remaining $25.00 auth released to cardholder.
→ Ledger: DEBIT network $75, CREDIT merchant $72.82, CREDIT fees $2.18
Case 2: Multi-capture (ship items separately)
→ Capture $40.00 (shipment 1)
→ Capture $35.00 (shipment 2)
→ Total captured: $75.00. Remaining $25.00 released.
→ Each capture is a separate ledger transaction.Authorization Expiry
Problem: Merchant authorizes $100 but never captures
Card network rules:
Visa: auth expires in 7 days
Mastercard: auth expires in 7-30 days (varies)
If not captured → funds released to cardholder automatically
Our handling:
Background job scans for old uncaptured auths
After 6 days: send webhook warning merchant
After 7 days: mark as expired, release ledger hold
Merchant can re-authorize if neededNetwork Timeout (Ambiguous Response)
Problem: We send auth request to Visa → timeout after 30s
Did Visa approve it? Decline it? Never receive it?
This is the HARDEST edge case in payments.
Solution: Status Inquiry + Reconciliation
1. Immediately: mark payment as "processing" (not succeeded/failed)
2. After 30s: send "status inquiry" to Visa
→ "I sent request REQ-123, what happened?"
→ Visa responds: approved / declined / not found
3. If "not found": safe to retry with new request ID
4. If approved/declined: update our state accordingly
5. If status inquiry also times out:
→ Mark as "requires_manual_review"
→ Nightly reconciliation batch compares our records
with network settlement files
→ Catches ALL discrepancies within 24 hoursDouble-Charge Prevention on Retry
Scenario:
1. Merchant calls POST /confirm (Idempotency-Key: IK-123)
2. We auth with Visa → approved → our server crashes BEFORE responding
3. Merchant times out → retries with same IK-123
4. We see IK-123 exists with status="processing" (not completed)
Handling:
→ Check: is there a network_txn_id for this payment?
→ Yes: we already sent to Visa and it was approved
→ Return the original approval result (don't send to Visa again)
→ Update idempotency cache with final result
If network_txn_id is NULL:
→ We crashed before sending to Visa
→ Safe to send authorization now (first attempt from network's POV)Dispute / Chargeback Handling
Customer calls bank: "I didn't make this purchase!"
Timeline:
Day 0: Issuer initiates dispute, debits merchant's account
Day 1: We receive dispute notification from network
→ Webhook: "charge.dispute.created"
→ Ledger: DEBIT merchant, CREDIT dispute_reserve
Day 1-21: Merchant submits evidence (receipt, tracking, logs)
→ We forward evidence to network (representment)
Day 30-75: Issuer reviews evidence
→ Won: funds returned to merchant (CREDIT merchant)
→ Lost: funds stay with customer (dispute_reserve cleared)
→ Webhook: "charge.dispute.closed" with outcome
Financial impact tracking:
dispute_rate = disputes / total_charges (by merchant)
If > 1%: warn merchant (Visa/MC programs penalize high dispute rates)
If > 2%: risk of losing processing privileges entirelyTradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Idempotency | DB-based (check before every write) | Redis SETNX + DB | Redis + DB | Redis for fast dedup (< 1ms); DB as durable backup |
| Ledger | Single-entry (simple balance tracking) | Double-entry bookkeeping | Double-entry | Provable correctness; audit trail; catches bugs via balance invariant |
| Money representation | Float/decimal | Integer cents (BIGINT) | Integer cents | Floats have rounding errors; $29.99 = 2999 cents; zero ambiguity |
| Card storage | Encrypt in main DB | Isolated Token Vault + HSM | Token Vault | Minimizes PCI scope from 100+ servers to < 5; massive compliance savings |
| Network routing | Single acquirer | Smart routing with fallback | Smart routing | 2-5% auth rate improvement = millions in recovered revenue |
| State transitions | Eventual consistency | ACID transactions | ACID | Money requires absolute correctness; payment + ledger + event = atomic |
| Webhook delivery | Synchronous (block on merchant) | Async with retry queue | Async + retry | Merchant downtime shouldn't block our pipeline; retry up to 3 days |
| Settlement | Real-time (per-transaction) | Daily batch | Daily batch | Reduces ACH costs; provides fraud review window; industry standard |
| Fraud scoring | Rules-only | ML model + rules | ML + rules | ML catches patterns rules can't; rules handle explicit policies |
| Consistency | Optimistic (retry on conflict) | Pessimistic (lock on payment) | Pessimistic | Payment state transitions must be serialized; no lost updates |
Reliability & fault tolerance
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
Single Points of Failure & MitigationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Disaster Recovery
Disaster RecoveryExcalidraw diagram · editable shapes · reveal step by stepExplore
Reconciliation — The Ultimate Safety Net
Reconciliation — The Ultimate Safety NetExcalidraw diagram · editable shapes · reveal step by stepExplore
Production architecture
Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Workshop note · added for the website’s common reading format
Rebuild Payment System from memory, then change one assumption: ten times more traffic, a new region, or a stricter consistency requirement. Which component must change first—and which does not?
Compare Orchestrator with the same boundary in a related design. Write down one alternative you rejected, what it would simplify, and when you would choose it instead. Follow the source link at the end of this article to explore the original document.
Interview playbook
Interview Tips
Lead with correctness, not performance — "The #1 requirement is money never gets lost or double-counted." Explain idempotency keys, ACID transactions, double-entry ledger. Performance is secondary to correctness for payments.
Draw the payment lifecycle state machine — requires_payment_method → requires_confirmation → requires_capture → succeeded → refunded. Show you understand the two-phase (auth + capture) model and why it exists (hotel holds, preorders).
Explain idempotency concretely — Walk through the retry scenario: merchant sends charge, times out, retries with same key. Show how SETNX in Redis prevents duplicate processing, and how stored results return the original response.
Double-entry ledger is the showstopper answer — Most candidates say "store balances in a column." Explain: every money movement creates debit + credit entries; sum must always be zero; corrections are reversing entries, never updates. This invariant catches bugs, fraud, and data corruption.
Use integers for money, never floats — $29.99 = 2999 cents as BIGINT. Floating point: 0.1 + 0.2 = 0.30000000000000004. In payments, one cent off in a billion transactions = audit nightmare.
Tokenization minimizes PCI scope — Card numbers never touch your main infrastructure. Token Vault (< 5 servers in PCI scope) vs entire platform (100+ servers). Dramatically reduces audit cost and security risk.
Network timeout is the hardest edge case — Don't handwave "just retry." Explain: timeout is ambiguous (approved? declined? lost?). Solution: status inquiry to network + nightly reconciliation as safety net.
Smart routing improves auth rates — Not all acquirers are equal. Routing based on BIN, country, card type, historical performance can lift auth rates 2-5%. At scale, this is millions in recovered revenue.
Reconciliation is the last line of defense — Three-way nightly comparison: our records vs network settlement files vs ledger balances. Any mismatch = alert. This catches everything that all other safeguards missed.
End with fraud and disputes — Mention ML scoring (< 10ms real-time), 3DS challenges for medium-risk, and the dispute lifecycle. Show you understand the business impact: > 1% dispute rate = card network penalties.