Online Payment System
Design an online payment system like Stripe or PayPal — merchants accept payments, funds settle to bank accounts, with PCI compliance, idempotency, fraud detection, and ledger a…
Introduction
Design an online payment system like Stripe or PayPal — merchants accept payments, funds settle to bank accounts, with PCI compliance, idempotency, fraud detection, and ledger accuracy. Money paths demand strong consistency, audit trails, and regulatory reporting.
HLD separates card data vault (PCI scope), ledger double-entry bookkeeping, and external processor integration. Never store raw PAN in application DB.
Understanding the topic
Key concepts
- Payment intent: create → authorize → capture → settle → payout.
- Idempotency-Key mandatory on all charge APIs.
- Double-entry ledger: debit/credit accounts always balance.
- Webhook from processor at-least-once — idempotent handler.
- Fraud scoring async before capture; block high risk.
- Reconciliation batch matches processor statement to internal ledger.
flowchart TBClient --> PG[Payment Gateway]PG --> LedgerPG --> FraudPG --> Bank
Internal architecture
Architecture overview
flowchart TBClient --> PG[Payment Gateway]PG --> LedgerPG --> FraudPG --> Bank
Step-by-step explanation
- POST /v1/charges Idempotency-Key → Payment API → Fraud check → Processor token charge.
- Ledger service records immutable journal entries PostgreSQL.
- PCI: card tokenized via processor.js — vault stores token only.
- Webhook /webhooks/processor verifies HMAC signature, updates payment state.
- Settlement cron aggregates merchant balance → ACH payout file.
- Outbox publishes PaymentCaptured events to Kafka for email/receipt.
Informative example
Idempotent charge with ledger double-entry in one transaction:
@Servicepublic class PaymentService {private final LedgerRepository ledger;private final ProcessorClient processor;private final IdempotencyStore idempotency;@Transactionalpublic ChargeResult charge(ChargeRequest req, String idempotencyKey) {return idempotency.execute(idempotencyKey, () -> {FraudScore score = fraud.score(req);if (score.blocked()) throw new FraudBlockedException();ProcessorResult pr = processor.charge(req.token(), req.amount(), req.currency());ledger.post(JournalEntry.builder().debit("cash:processor", req.amount()).credit("payable:merchant:" + req.merchantId(), req.amount()).reference(pr.transactionId()).build());return ChargeResult.from(pr);});}}
Ledger append-only. Reconciliation job flags mismatch. PCI scope minimized — no PAN in logs.
Real-world use
Real-world use cases
- E-commerce checkout Stripe integration.
- Marketplace split payments driver/restaurant/platform.
- Subscription billing recurring charges.
- Cross-border FX conversion fintech.
Best practices
- Immutable ledger — corrections via reversing entries.
- HMAC verify all webhooks.
- Separate PCI network segment for tokenization.
- Monitor authorization success rate and chargeback ratio.
- Daily reconciliation automated with alert on discrepancy.
- Audit log every state transition with actor and timestamp.
Common mistakes
- Store PAN or CVV — PCI scope explosion.
- Mutable balance column without journal — audit impossible.
- Webhook without idempotency — double credit merchant.
- Capture before fraud check completes.
- No reconciliation — silent money loss months later.
Advanced interview questions
Q1BeginnerWhy idempotency in payments?
Q2BeginnerAuthorize vs capture?
Q3IntermediateDouble-entry ledger purpose?
Q4IntermediateHandle processor webhook duplicate?
Q5AdvancedDesign Stripe-like platform for marketplaces.
Summary
Payment HLD prioritizes correctness, audit, and PCI minimization. Idempotency and ledger double-entry are non-negotiable. Tokenize cards — never store raw PAN. Webhooks processed idempotently with signature verification. Reconciliation catches processor drift. Notification system delivers receipts and alerts async.