Design a Payment System
Overview
A payment system processes financial transactions between buyers and sellers. It must guarantee exactly-once processing, handle idempotency, support multiple payment methods, and maintain strict consistency. Payment systems like Stripe, PayPal, and Square handle billions of dollars in transactions annually.
Requirements
Functional
- Accept payments (credit card, debit card, bank transfer, digital wallets)
- Process refunds
- Support multiple currencies
- Merchant payouts (settlement)
- Transaction history and receipts
- Fraud detection
- PCI DSS compliance
Non-Functional
- Scale: 100K+ transactions/second (peak), billions/year
- Latency: Payment authorization < 2 seconds
- Consistency: Exactly-once processing (no double charges, no lost payments)
- Availability: 99.999% (financial system)
- Security: PCI DSS Level 1 compliance, encryption at rest and in transit
- Auditability: Complete transaction log for every state change
Architecture
graph TB
subgraph "Client"
Merchant[Merchant]
Customer[Customer]
end
subgraph "API Layer"
LB[Load Balancer]
API[API Servers]
end
subgraph "Core Services"
PaySvc[Payment Service]
AuthSvc[Authorization Service]
CaptureSvc[Capture Service]
RefundSvc[Refund Service]
SettlementSvc[Settlement Service]
end
subgraph "Risk & Compliance"
FraudSvc[Fraud Detection]
ComplianceSvc[Compliance Engine]
end
subgraph "External"
CardNetwork["Card Networks<br/>(Visa, Mastercard)"]
Bank["Issuing Bank"]
Processor["Payment Processor<br/>(Stripe, Adyen)"]
end
subgraph "Data Stores"
PayDB[(Payment DB<br/>PostgreSQL)]
LedgerDB[(Ledger DB<br/>Immutable)]
IdempotencyDB[(Idempotency Store)]
end
subgraph "Messaging"
Kafka[Kafka<br/>Event Bus]
end
Merchant --> LB
Customer --> LB
LB --> API
API --> PaySvc
PaySvc --> AuthSvc
PaySvc --> FraudSvc
AuthSvc --> CardNetwork
CardNetwork --> Bank
PaySvc --> PayDB
PaySvc --> LedgerDB
PaySvc --> IdempotencyDB
PaySvc --> Kafka
Kafka --> CaptureSvc
Kafka --> SettlementSvc
SettlementSvc --> Merchant
Deep Dive: Payment Flow
Two-Step Payment (Auth + Capture)
sequenceDiagram
participant Customer
participant PaymentSvc
participant FraudCheck
participant Processor
participant CardNetwork
participant Bank
Customer->>PaymentSvc: Pay $100 (order_id: 123)
PaymentSvc->>PaymentSvc: Check idempotency
PaymentSvc->>FraudCheck: Risk assessment
FraudCheck-->>PaymentSvc: Risk: LOW
PaymentSvc->>Processor: Authorize $100
Processor->>CardNetwork: Authorization request
CardNetwork->>Bank: Check balance
Bank-->>CardNetwork: Approved (auth_code: ABC)
CardNetwork-->>Processor: Approved
Processor-->>PaymentSvc: Authorized
PaymentSvc->>PaymentSvc: Store auth record
PaymentSvc-->>Customer: Payment authorized
Note over PaymentSvc: Later: ship goods
PaymentSvc->>Processor: Capture $100
Processor->>CardNetwork: Capture request
CardNetwork->>Bank: Debit $100
Bank-->>CardNetwork: Captured
CardNetwork-->>Processor: Captured
Processor-->>PaymentSvc: Captured
PaymentSvc->>PaymentSvc: Update status = CAPTURED
PaymentSvc->>PaymentSvc: Create ledger entry
Why two-step?
- Authorization: Verifies the customer has funds and reserves the amount
- Capture: Actually transfers the money (after goods are shipped)
- Allows merchants to capture less than authorized (partial capture)
- Authorization expires if not captured (typically 7-30 days)
Deep Dive: Idempotency
Critical requirement: If the customer clicks “Pay” twice (or the request is retried), they should not be charged twice.
graph TB
Request["POST /pay<br/>idempotency_key: abc123"] --> Check{"Idempotency key<br/>exists?"}
Check -->|Yes| Return["Return previous result"]
Check -->|No| Process["Process payment"]
Process --> Store["Store result with key"]
Store --> Response["Return result"]
Implementation:
def process_payment(request, idempotency_key):
# Check if we've seen this key before
existing = idempotency_store.get(idempotency_key)
if existing:
return existing.result # Return cached result
# Process payment
result = charge_customer(request)
# Store result
idempotency_store.set(idempotency_key, result, ttl=24h)
return result
Idempotency key: A unique identifier (UUID) generated by the client for each payment attempt.
Deep Dive: Double-Entry Ledger
Every financial system uses double-entry bookkeeping:
graph LR
subgraph "Ledger Entry: Payment $100"
Debit["Customer Account<br/>Debit: $100"]
Credit["Merchant Account<br/>Credit: $100"]
end
Ledger entry:
{
"transaction_id": "txn_abc123",
"entries": [
{"account": "customer_123", "debit": 100.00, "currency": "USD"},
{"account": "merchant_456", "credit": 100.00, "currency": "USD"}
],
"timestamp": "2024-01-15T10:30:00Z",
"type": "PAYMENT",
"status": "COMPLETED"
}
Ledger rules:
- Immutable: Entries can never be modified or deleted
- Balanced: Total debits must equal total credits
- Append-only: New entries for corrections (reversals, refunds)
Deep Dive: Fraud Detection
graph TB
Transaction["New Transaction"] --> Rules["Rule Engine"]
Transaction --> ML["ML Model"]
Rules --> Score["Risk Score"]
ML --> Score
Score --> Decision{"Decision"}
Decision -->|Low risk| Approve["Approve"]
Decision -->|Medium risk| Review["Manual Review"]
Decision -->|High risk| Block["Block"]
Fraud signals:
- Velocity checks (too many transactions in short time)
- Geographic anomalies (card used in two countries simultaneously)
- Device fingerprinting
- Amount anomalies (unusually large transaction)
- Behavioral patterns (different from user’s normal behavior)
Deep Dive: Settlement
Settlement is the process of transferring funds from the payment system to the merchant.
graph LR
Transactions["Completed Transactions"] --> Aggregator["Daily Aggregator"]
Aggregator --> Payout["Payout Calculation"]
Payout --> Fees["Subtract Fees<br/>(2.9% + 30¢)"]
Fees --> Transfer["Bank Transfer<br/>(ACH/Wire)"]
Transfer --> Merchant["Merchant Account"]
Settlement flow:
- Aggregate all captured transactions for the merchant per day
- Calculate fees (percentage + fixed)
- Initiate bank transfer (ACH for US, SEPA for Europe)
- Merchant receives funds in 1-3 business days
Deep Dive: State Machine
stateDiagram-v2
[*] --> Created: Payment initiated
Created --> Authorized: Authorization approved
Created --> Failed: Authorization declined
Authorized --> Captured: Capture successful
Authorized --> Cancelled: Authorization expired/cancelled
Captured --> Refunded: Full refund
Captured --> PartiallyRefunded: Partial refund
PartiallyRefunded --> Refunded: Remaining refunded
Failed --> [*]
Cancelled --> [*]
Refunded --> [*]
Scalability
| Component | Strategy |
|---|---|
| API servers | Horizontal, stateless |
| Payment DB | PostgreSQL with read replicas, sharded by merchant_id |
| Ledger | Append-only, partitioned by time |
| Idempotency | Redis with TTL |
| Fraud detection | Real-time ML (low latency) + batch rules |
| Settlement | Batch processing (daily) |
| Event bus | Kafka for async processing |
Trade-Offs
| Decision | Benefit | Cost |
|---|---|---|
| Two-step auth+capture | Flexibility, prevents overselling | More complex flow |
| Idempotency keys | No double charges | Storage overhead |
| Double-entry ledger | Auditability, correctness | More complex writes |
| Async settlement | Decouples payment from payout | Delayed merchant funds |
| PostgreSQL over NoSQL | Strong consistency, ACID | Lower write throughput |
Interview Tips
- Start with idempotency — “The most critical requirement is that customers are never double-charged”
- Explain auth + capture — two-step payment for flexibility
- Discuss the ledger — double-entry, immutable, append-only
- Mention fraud detection — rule engine + ML model, real-time scoring
- Talk about PCI DSS — tokenization, encryption, never store raw card numbers
- Don’t forget settlement — daily aggregation, fee calculation, bank transfer
- Discuss state machine — created → authorized → captured → settled
Key Takeaways
- Payment systems use two-step processing: authorization (verify funds) + capture (transfer funds).
- Idempotency is critical: unique keys per payment attempt prevent double charges.
- Double-entry ledger: immutable, balanced (debits = credits), append-only for corrections.
- Fraud detection: real-time rule engine + ML model for risk scoring.
- Settlement: daily aggregation of captured transactions, minus fees, transferred to merchant.
- PostgreSQL for strong consistency; Kafka for async event processing.
- PCI DSS compliance: tokenization, encryption, never store raw card data.