Skip to content

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
12345678910CLIENTBuyer's browserSERVICECheckout APIDATABASEPostgresEXTERNALPayment providerSERVICEWebhook receiverWORKERFulfillmentworkerEXTERNALEmail providerWORKERReconciler

Select a component to see what it is responsible for and which state it owns.

  1. 1Buyer's browser → Payment provider: Card entry and 3-D Secure in hosted fields
  2. 2Buyer's browser → Checkout API: Pay (Idempotency-Key header); poll status
  3. 3Checkout API → Payment provider: Create payment with the attempt's key
  4. 4Checkout API → Postgres: Claim attempt; conditional transitions
  5. 5Payment provider → Webhook receiver: Signed events: at least once, unordered
  6. 6Webhook receiver → Postgres: Dedupe by event id; forward-only transition
  7. 7Fulfillment worker → Postgres: Claim outbox rows; record completion
  8. 8Fulfillment worker → Email provider: Send receipt with idempotency key
  9. 9Reconciler → Postgres: Attempts processing too long; ledger
  10. 10Reconciler → Payment provider: Look up by reference; settlement report
  • Request / response
  • Asynchronous

What you need to know

0 of 1 checks done
  1. A convincing guarantee walks through each way the bad outcome could happen and names the mechanism that stops it:

    ThreatMechanism
    Double-clickunique key on the attempt
    Retry after timeoutsame key sent to the provider
    Crash mid-flightattempt stays processing; resolved later
    Duplicate or late webhookforward-only transitions; effects on transitions

    Then it names the paths the mechanisms don't cover.

  2. Check

    Which of these is a real residual risk to state honestly?