ayushsalampuriya.xyz Revise

System Design · Design Question

Design a Payment System

Move money safely: idempotency, ledgers, PSP integration, webhooks, and reconciliation — correctness over cleverness.

1. Requirements

Ask: marketplace (split pay) vs simple merchant? multi-currency? Priority: no double charge > raw throughput

2. API

POST /v1/payments Headers: Idempotency-Key: ord_123_pay Body: { "orderId", "amount", "currency", "paymentMethodToken", "customerId" } → { "paymentId", "status": "pending|succeeded|failed" } POST /v1/payments/{id}/refunds { amount, Idempotency-Key } GET /v1/payments/{id} Webhook from PSP: POST /v1/psp/webhooks (signed)

3. Data model (ledger-centric)

payments(payment_id, order_id, amount, currency, status, psp_ref, created_at) idempotency_keys(key PK, response_body, payment_id) ledger_accounts(account_id, owner_type, currency) ledger_entries(entry_id, account_id, payment_id, amount_signed, ts) -- double-entry: sum(entries) per transfer = 0 refunds(refund_id, payment_id, amount, status)

Prefer append-only ledger entries over mutating a single balance row blindly. Balances = sum of entries (materialized for speed).

4. High-level design

Checkout → Payment API |-- check Idempotency-Key |-- create payment pending (DB TX) |-- call PSP (Stripe/Razorpay) with idempotency |-- update status + ledger entries |-- outbox → notify Order Service PSP webhooks → verify signature → update if newer state Reconciler job → download PSP reports → diff vs ledger

5. Idempotency deep dive

Same Idempotency-Key + same body → return stored response Same key + different body → 409 PSP call also uses key so retries do not double-charge

6. State machine

pending → succeeded → failed succeeded → refunded / partially_refunded Ignore stale webhook transitions (succeeded must not go back to pending)

7. Saga with orders

Reserve inventory → charge payment → confirm order. On payment fail, release inventory. On confirm fail after charge, refund. Orchestrator owns the workflow; each step idempotent.

8. Scaling

9. Security & compliance

10. Auth and capture

Some flows authorize first (hold funds) then capture later (after shipment). Model both as ledger states. Uncaptured auths expire — release holds with a job. Captures must reference the auth id idempotently.

authorized → captured → (optional) refunded → voided/expired

11. Multi-party marketplace (brief)

Quick revision

  • Idempotency keys on API and PSP calls prevent double charges.
  • Double-entry ledger is the source of financial truth.
  • Webhooks + reconciliation fix lost responses.
  • Strict payment state machine; ignore illegal transitions.
  • Saga with inventory/orders; compensations = refund/release.
  • Never store raw card PAN; use PSP tokens.
  • Correctness and auditability beat micro-optimizations.