Payment Processing & Ledger
Problem statement
Process card payments, support refunds, partial captures, disputes, and maintain an internal ledger that matches PSP settlements (Stripe, Adyen).
How it works
- PSP handles PCI vault, 3DS, network tokens.
- Your system records intent → authorized → captured → settled with double-entry ledger for money movement.
Analogy: A bank passbook where every rupee is both debited from one account and credited to another — the sum of all lines must always be zero (balanced books).
High-level design
Rendering diagram…
Components explained — this design
| Component | What it is | Why we use it here |
|---|---|---|
| Payments API | Your controlled surface for creating intents, captures, refunds. | Never expose raw PSP keys to browsers; centralizes validation and audit logging. |
| Payment intent state machine | Tracks requires_payment_method → succeeded etc. | Maps PSP states to internal accounting states; supports partial captures and disputes. |
| Ledger service | Double-entry journal of money movement. | Auditors and reconciliation require balanced books; append-only ledger prevents silent edits. |
| Stripe API | Card network gateway + webhooks for async outcomes. | Industry-standard PCI boundary; Radar fraud signals optional. |
| Signed webhooks | HTTP callbacks verified with shared secret signature. | Prevents forged payment events; always idempotent per event.id. |
| Reconciliation batch | Nightly job comparing PSP settlements vs ledger. | Catches missing webhooks, currency rounding, chargebacks. |
Shared definitions: 00-glossary-common-services.md
Low-level design
Webhooks
- Stripe: verify
Stripe-Signaturewith endpoint secret; process idempotently byevent.idin DynamoDB. - Azure: Logic Apps + Event Grid for webhook ingestion with replay support.
Ledger (double-entry)
| debit_account | credit_account | amount | currency | ref |
|---|---|---|---|---|
| gateway_clearing | revenue | 10000 | USD | pi_xxx |
| fees_expense | gateway_clearing | 320 | USD | fee_pi_xxx |
- PostgreSQL with SERIALIZABLE or append-only log table + materialized balance view updated async.
- Why not single column “balance”: race conditions; ledger is auditable.
Payouts (marketplaces)
- Stripe Connect — separate charges & transfers vs destination charges (tax/legal implications).
Idempotency
- All mutating APIs require Idempotency-Key header; store response body 24h.
E2E: successful capture
Rendering diagram…
Tricky parts
| Problem | Solution |
|---|---|
| Webhook arrives before API returns | Order state must tolerate out-of-order; use state machine |
| Partial refunds | New ledger entries reversing subset; never delete rows |
| FX | Store amount in minor units + FX rate snapshot id |
| Chargebacks | Dispute webhook → reserve from seller balance |
Caveats
- Strong consistency with external PSP is impossible — design for eventual + human ops tooling.
- PCI SAQ-A if only Stripe.js; broader scope if you touch PAN.
Compliance
- PSD2 SCA in EU — 3DS2 flows; network tokens for recurring.