Design a Payment System, stage 13 of 13: defend it
Defend the guarantee
A new engineer joins the payments team and asks: "How do we know we can't double-charge? And is there any way we still could?"
Answer in one page, the way you would in a design review or an interview.
System so far· 8 parts
Select a component to see what it is responsible for and which state it owns.
- 1Buyer's browser → Payment provider: Card entry and 3-D Secure in hosted fields
- 2Buyer's browser → Checkout API: Pay (Idempotency-Key header); poll status
- 3Checkout API → Payment provider: Create payment with the attempt's key
- 4Checkout API → Postgres: Claim attempt; conditional transitions
- 5Payment provider → Webhook receiver: Signed events: at least once, unordered
- 6Webhook receiver → Postgres: Dedupe by event id; forward-only transition
- 7Fulfillment worker → Postgres: Claim outbox rows; record completion
- 8Fulfillment worker → Email provider: Send receipt with idempotency key
- 9Reconciler → Postgres: Attempts processing too long; ledger
- 10Reconciler → Payment provider: Look up by reference; settlement report
- Request / response
- Asynchronous
What you need to know
0 of 1 checks done
A convincing guarantee walks through each way the bad outcome could happen and names the mechanism that stops it:
Threat Mechanism Double-click unique key on the attempt Retry after timeout same key sent to the provider Crash mid-flight attempt stays processing; resolved later Duplicate or late webhook forward-only transitions; effects on transitions Then it names the paths the mechanisms don't cover.
Check
Which of these is a real residual risk to state honestly?