Design Payment System
Card charges, idempotency, double-entry ledger, PCI boundaries, and reconciliation.
Interview tip Never store raw PAN. Use idempotency keys, ledger entries (debit/credit), async capture with PSP, and reconciliation jobs.
① Functional requirements
- Create payment intent with amount + currency
- Capture charge via card token
- Refund full or partial
- Idempotent API with client key
- Webhook to merchant on status change
- Merchant dashboard transaction history
② Non-functional requirements
- 10K TPS authorization peak
- Strong consistency for balances — no double charge
- PCI DSS — card data only at PSP/token vault
- Audit log immutable 7+ years
- 99.99% availability for authorize API
③ Back-of-the-envelope scale
Assumptions
- 10K TPS × $50 avg — high correctness not volume challenge
- Ledger append-only 100K entries/sec with sharding by merchant
- Idempotency store TTL 24h per key
- Reconciliation batch nightly with PSP settlement files
④ High-level architecture
Payment Platform
Merchant apps
Payment API (idempotent)
Ledger service (double-entry)
PSP / card network
Webhook dispatcher
Reconciliation jobs
API stores idempotency record first. Ledger writes in same DB transaction as payment state transition. PSP call async with timeout + polling.
⑤ Data flow & execution path
Charge execution
① Idempotency check→② Reserve ledger→③ PSP authorize→④ Capture / void→⑤ Webhook merchant
States: created → authorized → captured | failed | refunded
Double-entry: merchant_balance + platform_fees
Outbox table for reliable webhooks
Reconcile PSP settlement vs ledger daily
Walk failure at PSP timeout — payment stays authorized until expiry; merchant polls or webhook fires.
⑥ API & interfaces
| Endpoint / flow | Purpose | Notes |
|---|---|---|
| POST /v1/charges | Create charge | Idempotency-Key header |
| POST /v1/refunds | Refund | links charge_id |
| GET /v1/charges/{id} | Status | merchant scoped |
| POST /v1/webhooks/test | Simulate event | sandbox only |
⑦ Data model & storage
Payment: id, merchant_id, amount, status, idempotency_key. LedgerEntry: account, debit, credit, payment_id. Outbox: event payload.
| Store | What | Why |
|---|---|---|
| PostgreSQL | Payments + ledger | ACID transactions |
| Redis | Idempotency cache | fast duplicate detect |
| S3 | Settlement files | immutable audit |
⑧ Deep dive — core components
Idempotency and double charge prevention
Idempotency-Key → unique index. First request inserts row processing; duplicate returns same response body. Never call PSP twice for same key.
Double-entry ledger
Every money move = two entries balanced. Merchant +100 customer -100. Refund reverses entries. Audit reconstructs balance from entries only.
⑨ Trade-offs & alternatives
| Decision | Option A | Option B | Pick when |
|---|---|---|---|
| PSP sync | Sync authorize | Async queue | Sync simpler UX; queue for resilience |
| Ledger | SQL ACID | Event sourcing | SQL for financial correctness |
| Token | PSP tokenization | Own vault | Never own PAN — PCI scope |
| Webhook | At-least-once | Exactly-once | At-least-once + merchant idempotent |
⑩ 45-minute interview script
- 0–5 min: Charge/refund requirements
- 5–12 min: Correctness + PCI
- 12–22 min: API + idempotency
- 22–32 min: Ledger + states
- 32–40 min: Webhooks + reconciliation
⑪ Likely follow-up questions
| Question | Short answer |
|---|---|
| Multi-currency FX? | FX rate table at authorize time; ledger in merchant settlement currency |
| Dispute chargeback flow? | Separate dispute state; pull funds from merchant balance; evidence upload portal |
| 3DS authentication? | Redirect to issuer challenge before authorize; resume with session token |
⑫ Revision checklist
- Idempotency keys
- Payment state machine
- Double-entry ledger
- PSP token only
- Transactional outbox
- Webhook retries
- Reconciliation job
- Immutable audit log