1.Idempotency keys, immutable records, explicit states, webhooks, reconciliation
A payment flow is the standard example of a workflow you cannot make atomic: your database and the payment provider are two systems with no shared transaction, and money has already moved by the time you find out whether your own write succeeded. Six building blocks handle that, and they are not independent options — each one closes a gap the others leave open. An idempotency key, sent by you with the charge request, lets the provider recognise a repeated request as the same request and return the original result instead of charging again, which is what makes a retry after a timeout safe. Immutable transaction records mean you append what happened rather than updating a row in place, so a refund is a new row and the original charge is still readable — an audit trail you can reconcile against, rather than a current value you have to trust. Explicit payment states (`requires_payment_method`, `processing`, `succeeded`, `failed`) replace a boolean `paid` flag, so the very common in-between state has a name and the code has somewhere to put it. Webhook processing is how you learn the outcome of anything asynchronous, because a payment can complete minutes after the customer closed the tab. Reconciliation is a scheduled job that compares your records against the provider's and reports the differences, on the assumption that some will exist. And retry-safe operations means every step in the flow can run twice without a second effect — which is what the previous five blocks together are for.
Key features
- Idempotency key: a caller-generated key sent with the charge; the provider returns the original result for a repeat instead of charging twice — this is what makes retrying an ambiguous timeout safe
- Immutable transaction records: append a new row per event (authorised, captured, refunded) rather than updating a `paid` column, so history stays readable and reconcilable
- Explicit payment states: a named state machine (requires payment method → processing → succeeded / failed) instead of a boolean, because "in progress" is the state most flows spend real time in
- Webhook processing: the provider calls you when an asynchronous outcome is known; webhooks arrive out of order, more than once, and sometimes for a payment you have no record of yet
- Reconciliation: a scheduled job comparing your records against the provider's, designed on the assumption that discrepancies exist rather than that they should not
- Retry-safe operations: every step can run twice with one effect — the property the other five blocks exist to produce
Six building blocks and the specific failure each one closes
Three webhook properties you must design for, not hope against
- Your service → Your database: record intent (state: processing)
- Your service → Provider: POST charge, Idempotency-Key: order_8814
- Provider → Your service: connection times out — outcome unknown (money may or may not have moved)
- Your service → Provider: POST charge again, SAME idempotency key
- Provider → Your service: original charge returned, not a second one
- Provider → Your service: webhook: payment_intent.succeeded (may arrive before or after the retry)
- Your service → Your database: append event, advance state to succeeded
Command
POST /v1/payment_intents HTTP/1.1
Host: api.stripe.com
Idempotency-Key: order_8814_attempt_1
Content-Type: application/x-www-form-urlencoded
amount=4200¤cy=usd&customer=cus_...
# Same key again -> the original PaymentIntent is
# returned. No second charge is created.Six blocks, each closing a gap the others leave: idempotency keys make retrying an ambiguous timeout safe; immutable event rows keep history reconcilable; explicit states give the in-between outcome a name; webhook processing carries asynchronous results, deduplicated on the provider's event id and claimed before the effect is applied; reconciliation is a scheduled job that assumes discrepancies exist; and retry safety is the property all five together produce. The hard case is not failure — it is not knowing.