Skip to content

Design a Payment System, stage 11 of 13: change it

Adding refunds

A refund is money moving the other way, through the same provider, with the same latency and the same uncertainty.

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 2 checks done
  1. A refund is money moving the other way through the same provider, with the same latency, timeouts and uncertainty as a charge. Everything that made charges safe applies again: a durable record before the call, an idempotency key, a state for "unknown", and resolution from the provider.

  2. Think first

    The handler sets the attempt to 'refunded', revokes access, then calls the provider's refund endpoint, which times out. What's wrong?