Payment System
Stripe / PayPal
Design a service that lets merchants charge cards, refund and pay out, where no retry ever charges a customer twice and every cent is accounted for in a ledger that balances.
Overview
A payment service sits between merchants and the card networks. A merchant calls it to charge a customer's card; the service asks a card processor to authorize the amount, later captures it, eventually receives the money in a settlement, and pays the merchant out minus a fee. Refunds run the same path backwards.
The traffic is modest next to a social network: a few thousand payments a second at peak. What makes it hard is correctness under failure. A request can time out after the processor has charged the card, a client can retry, a worker can crash between two writes. The design has to guarantee that a customer is charged at most once for one purchase, that no money goes missing, and that you can prove both. Three tools do most of the work: idempotency keys, a payment state machine that never guesses an outcome, and a double-entry ledger checked by reconciliation. The Idempotency, Outbox Pattern and Saga topics cover those techniques in general; this page applies them.
Questions to ask first
- Who are the users? — Merchants calling an API; their customers pay by card
- Volume? — 50 million payments a day, $50 average, peak 5× average
- Separate authorize and capture? — Yes, for merchants that ship later; one-step charges for the rest
- Do we move the money ourselves? — No: card processors do; we record, instruct and reconcile
- Consistency? — Money movement is never eventually consistent: every step is recorded durably before and after
- Card data? — Stored only in an isolated vault; everything else sees tokens
What you will be able to do
- Make a charge safe to retry end to end, from the merchant's request to the processor call.
- Walk a payment through authorize, capture, refund and settlement, and say what the ledger records at each step.
- Build a double-entry ledger in SQL that cannot hold an unbalanced entry.
- Handle a processor call whose outcome is unknown, and find mistakes with reconciliation.
- Keep card numbers out of most of the system.
Key concepts
- Idempotency keys
- Authorize and capture
- Payment state machine
- Unknown outcomes
- Double-entry ledger
- Integer minor units
- Reconciliation
- Outbox events
- Webhooks
- Tokenization
Functional requirements
- Create a payment: authorize a card, and capture now or later
- Refund all or part of a captured payment
- Record every movement of money in a ledger
- Notify merchants of payment events
- Reconcile our records with processor settlement reports daily
- Pay merchants their balance
Non-functional requirements
- A retried request never charges twice
- No acknowledged payment is lost; the ledger always balances
- Authorization responds in under 2 seconds at p99, most of it the processor (a target we chose)
- 99.99% availability for the payments API (a target we chose)
- Card numbers only inside the vault
Interview questions
- How do you make sure a customer is never charged twice?
- Explain double-entry bookkeeping for a payment.
- What happens when the processor call times out?
- Why separate authorization from capture?
- How does reconciliation work?
- How do you deliver events to merchants reliably?
- How do you keep card numbers safe and limit PCI scope?
- How would you scale this?
Sign up free to read the full topic
More System Design topics
Start free on Cariara · Coding problems · System design studio