Design a Payment System
hardA payment system processes charges, refunds, transfers, and the bookkeeping that records them. Stripe, Adyen, Square, PayPal all share the architecture: idempotency on every API call, durable state machines, a strongly consistent ledger, and rigorous reconciliation against the bank/card-network statements.
Key Concepts
Idempotency-Key header. First call executes, response stored. Replay with same key returns stored response — doesn't re-execute. Networks between merchant ↔ gateway ↔ network are lossy; a 200 OK lost in transit looks identical to a charge that never happened. Idempotency keys retained 24h+ (outlast retry windows). Stripe's published design retains them this long.-$100 to customer account, +$100 to merchant revenue. Balances computed via SUM(entries WHERE account = X) — never mutate stored balances. Append-only; historical state always recoverable. Postgres or Spanner-class. Schema is the hardest part to change later — design carefully.authorized → captured → settled (or voided, refunded). Each transition persisted before any external call; recovery walks from persisted state. Saga for cross-service flows: sequence of local transactions, each with a compensating action if the next fails (charge → ship → email; ship fails → refund). 2PC rarely used — doesn't compose across organizations.High-level design
API: idempotency-key → idempotency store check → execute or replay.
Charge flow: validate → fraud check → call PSP/network → record outcome → ledger entry.
Ledger: Postgres / Spanner; double-entry; append-only.
Multi-step: state machine in DB; saga for cross-service.
Settlement: nightly reconciliation against bank/network files.
Webhooks: at-least-once delivery to merchants with signature.
Components
- API gateway + idempotency store (Redis or Postgres).
- Fraud / risk service (rules + ML).
- Tokenization vault (PCI-scoped, isolated).
- PSP / network integration (Visa, Mastercard, ACH).
- Ledger (Postgres / Spanner / CockroachDB) with double-entry.
- State machine / saga orchestrator.
- Reconciliation jobs (Spark over ledger + statements).
- Webhook dispatcher (signed, retried).
- Settlement engine.
- Dispute / chargeback workflow.
Idempotency in depth
Key passed by client (UUID or content hash).
First call: lock on (idempotency_key); execute; store (request hash, response).
Subsequent calls: return stored response. Validate request hash matches (else: 422 with different body).
Retention: 24h+ (outlast retry windows).
Edge case: first call fails mid-write. Use a status field (pending → completed) so retries can resume safely.
Ledger design
Double-entry: every transaction has 2+ entries summing to zero.
Append-only: never UPDATE; corrections are new offsetting entries.
Balance: SUM(entries WHERE account = X). Cache for read performance.
Schema: (transaction_id, account, debit, credit, currency, timestamp, metadata).
Multi-currency: keep entries in transaction currency; FX rates as separate ledger.
Audit: immutable; complete history of every cent.
Saga pattern
Multi-step flow with compensations: each step's failure triggers undo for prior steps.
Orchestrator-based: central saga manager dispatches steps and compensations.
Choreography-based: services react to events; no central coordinator.
Example: charge → ship → email. If ship fails, refund (compensation for charge).
Idempotency on each step + compensation makes it safe under retries.
Trade-offs
Strong consistency in ledger non-negotiable: Postgres or Spanner-class.
Idempotency keys must outlive retry windows (24h+).
Sagas vs 2PC: sagas compose across services and orgs; 2PC requires shared transaction manager.
Networks are async — design for delayed authorization callbacks.
Never silently retry payment — always check status first.
Multi-region: usually region-pinned with cross-region reconciliation; truly global ACID via Spanner is heavyweight.