Design a Payment System, stage 3 of 13: decide
Where does payment state live?
A buyer may try one card, get declined, and pay with another. A payment may sit in "processing" for minutes. Finance needs to know exactly which provider event made an order paid, and support needs to answer "why was I charged?" months later.
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
What you need to know
A boolean can say "paid" or "not paid". A payment can be in more states than that: we asked and don't know yet, declined (with a reason), succeeded, refunded. And one order can have several attempts: a declined card, then a different card.
A model with too few states forces you to guess whenever reality is in a state you can't represent.
Check
Payment state is a 'paid' boolean on the order. The provider call times out. What do you store?Two kinds of table solve different problems:
- Current state, one row per attempt, updated as it moves: what decisions read.
- An append-only ledger, one row per change and never updated: who or what changed the state, when, and on which evidence. It's what finance and support read. See Append-only logs.
Writing both in the same transaction means they can never disagree.
Think first
A buyer's first card is declined and they pay with a second. Why does each attempt need its own idempotency key?