Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Aggregate all captured transactions for the merchant per day
  2. Calculate fees (percentage + fixed)
  3. Initiate bank transfer (ACH for US, SEPA for Europe)
  4. 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

ComponentStrategy
API serversHorizontal, stateless
Payment DBPostgreSQL with read replicas, sharded by merchant_id
LedgerAppend-only, partitioned by time
IdempotencyRedis with TTL
Fraud detectionReal-time ML (low latency) + batch rules
SettlementBatch processing (daily)
Event busKafka for async processing

Trade-Offs

DecisionBenefitCost
Two-step auth+captureFlexibility, prevents oversellingMore complex flow
Idempotency keysNo double chargesStorage overhead
Double-entry ledgerAuditability, correctnessMore complex writes
Async settlementDecouples payment from payoutDelayed merchant funds
PostgreSQL over NoSQLStrong consistency, ACIDLower write throughput

Interview Tips

  1. Start with idempotency — “The most critical requirement is that customers are never double-charged”
  2. Explain auth + capture — two-step payment for flexibility
  3. Discuss the ledger — double-entry, immutable, append-only
  4. Mention fraud detection — rule engine + ML model, real-time scoring
  5. Talk about PCI DSS — tokenization, encryption, never store raw card numbers
  6. Don’t forget settlement — daily aggregation, fee calculation, bank transfer
  7. 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.

Cross-References