Stripe System Design: Why Payments Are Different
Most apps can survive a bug that shows the wrong profile picture. A payments system can't survive charging someone twice, losing a payment, or showing the wrong balance. Stripe handled $1.9 trillion in total payment volume in 2025, up 34%, so even a tiny error rate means real money going wrong.
That makes Stripe a great system design case study. The interesting part isn't raw scale. It's correctness under failure: networks drop, servers crash, and banks time out, but money must still add up exactly.
🎯 Quick Answer (30-Second Read)
- The flow: your server creates a PaymentIntent, the browser collects card details directly with Stripe, Stripe talks to card networks and banks, then notifies you with a webhook.
- Idempotency keys make retries safe, so a network glitch never charges twice.
- A double-entry ledger records every money movement as balanced debits and credits.
- Webhooks deliver events asynchronously with retries, since many payment steps happen later.
- API versioning keeps old integrations working for years while the API evolves.
- Sensitive card data stays inside Stripe's PCI-compliant systems, not yours.
The Payment Flow
Why card details skip your server: Stripe's checkout components send card data straight to Stripe. Your systems only see a token or payment method ID, which keeps most of the PCI DSS compliance burden off you.
Why the webhook matters: the customer might close the tab before your success page loads. The webhook is the reliable signal to fulfil the order.
High-Level Architecture
(versioned)"] --> IDEM[("Idempotency store")] API --> PAY["Payments service
PaymentIntent state machine"] PAY --> RISK["Fraud & risk scoring
(Radar)"] PAY --> VAULT[("Card vault
PCI-isolated")] PAY --> ROUTE["Network routing"] ROUTE --> NET["Card networks, UPI,
wallets, bank rails"] PAY --> LEDGER[("Double-entry ledger")] PAY --> EVT[["Event stream"]] EVT --> WH["Webhook delivery
with retries"] LEDGER --> PAYOUT["Payouts to merchants"] LEDGER --> RECON["Reconciliation
vs bank reports"]
| Component | Job |
|---|---|
| Versioned API | Stable contract for millions of integrations |
| Idempotency store | Remembers results of requests by key |
| PaymentIntent state machine | Tracks each payment through its steps |
| Risk scoring | Flags likely fraud before authorising |
| Card vault | Isolates card numbers in a locked-down environment |
| Ledger | Source of truth for every rupee and dollar |
| Webhooks | Tell merchants what happened, reliably |
| Reconciliation | Checks Stripe's records match banks' records |
1. Idempotency: Never Charge Twice
A request can succeed on Stripe's side while the response is lost. If your server retries blindly, the customer pays twice. Stripe's fix is the Idempotency-Key header:
- Your server sends a unique key with each create request.
- Stripe saves the status code and body of the first request for that key and returns the same result for retries.
- Reusing a key with different parameters returns an error.
- Keys can be removed once they're at least 24 hours old.
For the full pattern, see Idempotency Explained.
2. The Payment as a State Machine
These are the real statuses a Stripe PaymentIntent moves through. Explicit states mean every payment has exactly one current status, and every transition can be logged and audited.
3. Double-Entry Ledger: Money Must Balance
Every money movement is recorded as at least two entries that sum to zero:
| Account | Debit | Credit |
|---|---|---|
| Customer card funds | ₹999 | |
| Merchant balance | ₹970 | |
| Stripe fee revenue | ₹29 | |
| Total | ₹999 | ₹999 |
If debits and credits ever don't match, something is wrong and can be traced. Ledgers are append-only: you never edit an entry, you add a correcting one. That gives a full audit trail.
4. Webhooks: Telling Merchants Reliably
Many payment events happen later: a bank transfer clears, a dispute opens, a subscription renews. Stripe sends these as webhooks to your endpoint.
- Stripe retries failed deliveries with backoff, so your endpoint must handle the same event more than once.
- Each webhook is signed, and your server must verify the signature before trusting it.
- Respond quickly with 2xx and do slow work in a background job.
5. API Versioning: Not Breaking Millions of Integrations
Stripe pins each account to an API version, so existing code keeps getting responses in the shape it expects even as the API changes. Internally, Stripe has described transforming responses backwards through version change modules, so new code is written once against the latest version and older versions are produced by applying changes in reverse.
Lessons for Any System Handling Money
- Make every write idempotent
- Model each payment as an explicit state machine
- Use an append-only, double-entry ledger as the source of truth
- Assume webhooks and events arrive late, twice or out of order
- Reconcile your records against external ones daily
- Keep card data out of your systems entirely
Frequently Asked Questions
How does Stripe prevent duplicate charges?
Stripe supports idempotency keys. Clients send a unique Idempotency-Key header with each request, and Stripe stores the first response for that key, returning it for any retry instead of processing the payment again. Keys can be removed once they are at least 24 hours old.
What is a PaymentIntent in Stripe?
A PaymentIntent is Stripe's object for tracking a single payment from creation to completion. It moves through statuses such as requires_payment_method, requires_action, processing and succeeded, handling steps like 3-D Secure authentication along the way.
How would you design a payment system in an interview?
Cover idempotent APIs, a payment state machine, a double-entry ledger as the source of truth, asynchronous webhooks with retries and signatures, integration with external payment networks, and daily reconciliation. Emphasise correctness and auditability over raw throughput.
Conclusion
Stripe's system design is built around correctness: idempotency keys stop double charges, PaymentIntents track each payment as a state machine, a double-entry ledger keeps money balanced, webhooks deliver late events reliably, and API versioning protects millions of integrations. Those patterns apply to any product that moves money.
Sources
- Stripe: 2025 annual letter ($1.9T total payment volume)
- Stripe docs: How PaymentIntents work
- Stripe API reference: Idempotent requests
- Stripe docs: Webhooks
- Stripe blog: APIs as infrastructure, future-proofing Stripe with versioning
Last verified: 2 November 2026.
Related reads: Idempotency Explained · How Does UPI Work? · Message Queue Explained · How Does Google Pay Make Money?
I write about system design and how real products are built. Follow on LinkedIn or X, or invite me to speak.