System Design

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 / flowPurposeNotes
POST /v1/chargesCreate chargeIdempotency-Key header
POST /v1/refundsRefundlinks charge_id
GET /v1/charges/{id}Statusmerchant scoped
POST /v1/webhooks/testSimulate eventsandbox only

⑦ Data model & storage

Payment: id, merchant_id, amount, status, idempotency_key. LedgerEntry: account, debit, credit, payment_id. Outbox: event payload.
StoreWhatWhy
PostgreSQLPayments + ledgerACID transactions
RedisIdempotency cachefast duplicate detect
S3Settlement filesimmutable 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

DecisionOption AOption BPick when
PSP syncSync authorizeAsync queueSync simpler UX; queue for resilience
LedgerSQL ACIDEvent sourcingSQL for financial correctness
TokenPSP tokenizationOwn vaultNever own PAN — PCI scope
WebhookAt-least-onceExactly-onceAt-least-once + merchant idempotent

⑩ 45-minute interview script

  1. 0–5 min: Charge/refund requirements
  2. 5–12 min: Correctness + PCI
  3. 12–22 min: API + idempotency
  4. 22–32 min: Ledger + states
  5. 32–40 min: Webhooks + reconciliation

⑪ Likely follow-up questions

QuestionShort 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
paymentsfintechledgeridempotencypci