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