Design a Payment System, stage 5 of 13: break it
A buyer was charged twice
This is the prototype's checkout handler. It looks reasonable. Find every line that contributes to double charges or inconsistent state.
System so far· 4 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 → Postgres: Claim attempt; conditional transitions
What you need to know
0 of 1 checks done
Check-then-act reads a value, decides, then acts: "if the order isn't paid, charge it". Two requests running at once can both read "not paid" before either acts, and both charge.
Only an operation the database performs atomically, like inserting under a unique constraint, can decide "first or not" correctly when requests overlap.
Check
A buyer double-clicks Pay. Both requests read status = 'pending', then each calls the provider without an idempotency key. What does the provider see?Look for these in any payment handler:
- Is something durable written before the irreversible call? If not, a crash at that moment leaves no trace that a charge may exist.
- Is the same key sent on every retry? If not, retries become new charges.
- Are follow-up actions recorded with the state change? If not, a crash after the commit loses them.