Interviewer
Design a payment gateway — like Stripe or Adyen. Merchants integrate with our API to charge customers. We process credit cards, handle refunds, and deliver webhook notifications. Assume 50,000 transactions per second at peak.
Candidate
Before I draw any boxes, I want to establish the single most important invariant in a payment system: every API call must be idempotent. Network failures, client retries, and load balancer re-routes mean that any request might arrive twice. If a merchant sends a charge request and gets a timeout, they'll retry — and we must guarantee that the charge happens exactly once, not twice. So the first thing I'd design is the idempotency layer. Every API request carries a merchant-provided Idempotency-Key header. On the first request, we create a record in an idempotency table: (idempotency_key, merchant_id, request_hash, status, response). Before processing any request, we check this table. If we find a completed entry with a matching request hash, we return the stored response. If we find an in-progress entry, we return a 409 Conflict. If no entry exists, we insert one with status=PROCESSING and proceed. The idempotency record has a 24-hour TTL.
📝 Annotation
Starting with idempotency before being asked shows depth. This is the foundational guarantee of any payment system, and most candidates don't mention it until prompted. The request-hash check prevents a subtle bug where a client reuses an idempotency key with different parameters.
Interviewer
Good foundation. Walk me through the payment processing flow.
Candidate
The core payment flow has two phases: authorization and capture. This is the auth/capture split that every card network uses. When a merchant calls POST /charges, we first authorize: we send the card details (tokenized — we never store raw PANs) to the acquiring bank, which routes to the card network (Visa/Mastercard), which routes to the issuing bank. The issuing bank checks funds availability and returns an auth code. At this point, money isn't moved — it's just reserved. The merchant then calls POST /charges/{id}/capture within 7 days to actually move the money. Some merchants use auto-capture (authorize and capture in one step), but separating them gives flexibility for delayed fulfillment (e.g., charge when the item ships). Refunds reverse the flow: we send a refund request through the same chain. Partial refunds are supported by specifying an amount less than the original charge.
📝 Annotation
Explaining the auth/capture split with the 7-day capture window shows the candidate understands payment network mechanics, not just API design. Mentioning that raw PANs are never stored (tokenization) signals PCI-DSS awareness.
Interviewer
How do you design the ledger?
Candidate
The ledger is the source of truth for all money movement and must be append-only — we never update or delete entries. I'd use a double-entry accounting model. Every financial event creates at least two entries that sum to zero. When a merchant charges $100: debit the customer's liability account $100, credit the merchant's receivable account $100. When we settle with the merchant (minus our 2.9% fee): debit the merchant's receivable $100, credit the merchant's bank payout $97.10, credit our revenue account $2.90. This makes reconciliation trivial — the sum of all entries must always be zero. If it's not, something is wrong. The ledger table is sharded by merchant_id and partitioned by date. Each entry has: entry_id, transaction_id, account_id, amount (signed), currency, created_at, and a reference to the originating event. We run hourly reconciliation jobs that verify the zero-sum invariant per transaction and flag discrepancies for investigation.
📝 Annotation
The double-entry model with the zero-sum invariant is exactly how production payment systems work. Showing the fee split (merchant payout + revenue account) and the hourly reconciliation job demonstrates that the candidate understands the full lifecycle of a payment, not just the API layer.
Interviewer
Tell me about webhook delivery. How do you notify merchants reliably?
Candidate
Webhook delivery is an at-least-once system. When a payment event occurs (charge succeeded, refund completed, dispute opened), we write a webhook event record to a database table with status=PENDING. A delivery service polls for pending events (or listens to a Kafka topic) and sends an HTTP POST to the merchant's configured URL. The payload includes the event type, a timestamp, and the full resource object. We sign the payload with an HMAC-SHA256 using a per-merchant secret so they can verify authenticity. If the merchant returns a 2xx, we mark the event as DELIVERED. If they return a non-2xx or time out (10-second timeout), we retry with exponential backoff: 1 min, 5 min, 30 min, 2 hours, 8 hours, 24 hours — 6 retries over roughly 34 hours. After all retries are exhausted, we mark it FAILED and surface it in the merchant dashboard. The merchant can then manually retry or fetch missed events via a GET /events API with pagination. Crucially, merchants must implement idempotent webhook handlers because they might receive the same event twice.
Interviewer
How do you handle fraud detection at this scale?
Candidate
Fraud detection sits in the authorization path but must not add significant latency — we budget 50ms for the fraud check within the overall 500ms auth SLA. I'd use a two-tier system. The first tier is a real-time rules engine: velocity checks (more than 5 transactions in 10 minutes from the same card), geographic anomalies (card used in NYC and London within an hour), BIN-country mismatches, and known-bad lists. This runs in-memory and decides in under 10ms. The second tier is an ML model — a gradient-boosted tree trained on historical fraud labels. Features include transaction amount relative to card average, merchant category, time since last transaction, device fingerprint similarity, and shipping-billing address distance. The model returns a fraud probability score. If it's above 0.8, we decline. Between 0.5 and 0.8, we trigger 3D Secure (the "verify your identity" step). Below 0.5, we proceed. We retrain the model weekly on new labeled data and A/B test model versions to measure precision/recall improvements. False positives are expensive — a declined legitimate transaction costs us the merchant relationship — so we optimize heavily for precision.
📝 Annotation
The two-tier fraud system (rules + ML) with specific latency budgets and threshold actions (decline / 3DS / proceed) is production-grade. Mentioning the cost of false positives and optimizing for precision shows business awareness, not just technical correctness.
Interviewer
What about PCI compliance? How do you handle sensitive card data?
Candidate
PCI-DSS compliance is non-negotiable. We minimize our cardholder data environment (CDE) — the set of systems that touch raw card data. The merchant never sends us raw card numbers. Instead, they use our JavaScript SDK (like Stripe Elements) which captures card details in an iframe hosted on our domain and sends them directly to our tokenization service. This service runs in an isolated PCI-scoped network segment with its own VPC, encrypted at rest and in transit, access logged and audited. It returns a token (tok_xxx) that the merchant uses in API calls. The tokenization service stores the mapping (token → encrypted PAN) in an HSM-backed vault. Only the tokenization service and the payment processor gateway can decrypt PANs. All other services only ever see tokens. We undergo annual PCI-DSS Level 1 audits, and the CDE is penetration-tested quarterly. Key rotation happens every 90 days with zero downtime using dual-key decryption during the rotation window.
📝 Annotation
The CDE minimization strategy with isolated VPC, HSM-backed vault, and iframe-based card capture is how Stripe actually works. Mentioning key rotation with dual-key decryption shows operational maturity.
Interviewer
Final question: how do you handle multi-currency?
Candidate
Multi-currency adds complexity at every layer. The merchant specifies a charge currency (say EUR), but the cardholder's bank settles in their local currency (say USD). The conversion happens at the card network level — Visa/Mastercard apply their exchange rate plus a markup. In our system, we store all amounts in the smallest currency unit (cents, pence, etc.) as integers to avoid floating-point errors. The ledger records the merchant-facing amount in the charge currency and the settlement amount in the settlement currency, with the exchange rate captured at transaction time. For merchants who want to present prices in the customer's currency (dynamic currency conversion), we offer a rates API that quotes a locked rate valid for 30 minutes. The rate comes from a feed we ingest from a forex provider every 60 seconds. The critical rule: never do currency math with floats. Everything is integer arithmetic in minor units.
📝 Annotation
The "never use floats for money" rule and integer minor-unit storage is fundamental but often overlooked. Capturing the exchange rate at transaction time for ledger accuracy shows the candidate understands the reconciliation implications of currency conversion.