Revision sheet
Design a payment system like Stripe.
The six things worth holding in your head the night before. Everything else is on the full page.
The opening move — minute one
“100M a day is about 1,160 TPS — this isn’t a scale problem, it’s a correctness problem. I’m going to design a small ACID core that can’t lose or duplicate money, and keep everything that tolerates delay async.”
The state machine
CREATED → PROCESSING → AUTHORIZED → CAPTURED → SETTLED
FAILED (from PROCESSING: declined / timeout) · VOIDED (from AUTHORIZED: released, never captured) · REFUNDED (from CAPTURED or SETTLED: via a new refund transaction — never deletion)
Must-haves
- Treats idempotency as a first-class concern: client-supplied idempotency keys with 24h TTL, stored in Redis or DB, replay returns the original response.
- Designs a double-entry ledger (every transaction = 2 entries: debit + credit) and explains why a single ‘balance’ table is wrong.
- Names the payment state machine explicitly: CREATED → PROCESSING → AUTHORIZED → CAPTURED → SETTLED, and what triggers each transition.
- Sizes throughput (~1,160 TPS avg, ~3,500 TPS peak), ledger growth (~200M rows/day), 7-year retention (~256 TB).
- Addresses PCI DSS scope: tokenization at the ingress, raw card data never stored in our systems, all storage references tokens.
Red flags
- Updating a single ‘account_balance’ column instead of using a ledger — corrupts under any concurrent write or partial failure.
- No idempotency story — client retries result in duplicate charges, every time.
- Storing raw card numbers anywhere in the application — PCI DSS violation, $1M+/yr compliance cost increase.
- Treating the PSP (Stripe / Adyen) as ‘just call this API’ with no retry / state reconciliation — guarantees stuck transactions.
The ten trap claims — each one is wrong
- “payment is a single synchronous step”
- “just check if payment already exists”
- “the PSP handles idempotency”
- “eventual consistency is fine for payments”
- “refund just deletes the payment record”
- “encrypt the card numbers and store them”
- “update the balance column directly”
- “fire-and-forget webhooks”
- “publish to Kafka then write to DB”
- “Kafka guarantees global ordering”
The L4 lines — what depth sounds like
Idempotency & Duplicate Prevention
Quantifies the idempotency store size (100M keys × 256 bytes ≈ 25 GB), addresses key collision probability, cache vs DB tradeoff for lookup latency, and idempotency key scoping per merchant to prevent cross-tenant collisions
Double-Entry Ledger
Quantifies ledger write volume (1,160 TPS × 2 entries × 1 KB ≈ 200 GB/day), addresses ledger sharding by merchant_id, hot-merchant write contention mitigation via batching, and cross-currency ledger entries with conversion rate snapshotting
Saga / Distributed Transaction Orchestration
Addresses partial failure scenarios (capture succeeds at PSP but local DB write fails), stuck saga detection via heartbeat TTLs, compensation ordering constraints, and poison-pill saga isolation to prevent cascading failures
PCI Compliance & Tokenization
Addresses multi-PSP token portability (network tokens vs PSP-specific tokens), PCI DSS Level 1 audit requirements, key rotation schedules, and the tradeoff between in-house vault (control, full PCI scope) vs processor tokenization (smaller scope, vendor lock-in)
Settlement Reconciliation
Quantifies reconciliation load (100M transactions × 3 sources = 300M comparisons nightly), addresses multi-PSP reconciliation with different file formats, real-time vs batch tradeoffs, and regulatory reporting requirements for unresolved mismatches
Multi-Currency Support
Addresses cross-border payment routing (choose PSP based on currency pair for best FX rate), hedging strategies for FX exposure between authorization and settlement, and multi-currency ledger entries with realized vs unrealized gain/loss tracking