Playbook
Payment Failure Taxonomy (Payment Failure Taxonomy)
A timeout, a 429, a 5xx, a business decline, and an infrastructure fault are not the same failure. Each category needs its own retry policy.
Distributed Payment Engine
Part 11 of 22
A series on distributed payment architecture — the gap between capture and complete.
The previous part showed how semantic events like PaymentFailed hide the provider's status codes. But a single PaymentFailed event still isn't enough, because the word 'failed' covers very different realities.
A declined card, a network timeout, a PSP returning 429, and a PSP returning 500 can all look like 'failure' — yet each demands a completely different response. This part builds a taxonomy that makes those differences visible.
PaymentFailed
├─ Business Decline (card rejected — do not retry)
├─ Timeout (ambiguous outcome — retry carefully)
├─ Rate Limited (429) (too many requests — retry with backoff)
└─ Infrastructure (5xx) (provider-side fault — retry)
Where the concepts first show up
📦 Business decline
The PSP rejects the request for a reason tied to the card itself: insufficient funds, suspected fraud.
📦 Transient failure
A temporary fault where retrying the same request makes sense: timeout, 5xx, 429.
📦 Permanent failure
A fault where retrying changes nothing: an invalid card number, an unsupported currency.
📦 Ambiguous outcome
A state where you don't know if the request ever reached the PSP: a connection timeout.
Retrying a business decline wastes time; failing to retry an ambiguous outcome risks a real missed payment. The taxonomy exists to keep these two risks apart.
The four core categories
Business decline: the PSP received the request, processed it, and made a decision — the card was rejected. This isn't a system fault, it's a business decision. Retrying won't change the outcome; the right move is offering the customer another payment method.
Timeout / ambiguous outcome: the request went out but no response ever arrived. The danger here is that the payment may have already succeeded on the PSP's side — only the response got lost on your end. This category can't be handled with a blind retry; you must first query the actual status (using the idempotency key), then decide.
Rate limited (429): the PSP is temporarily rejecting you to control request volume. This isn't a fault, it's a signal. Retrying immediately makes things worse; you need to wait with backoff.
Infrastructure (5xx): something failed on the PSP's own side. The request was never processed, so retrying is generally safe — but a sustained stream of 5xx responses is a circuit-breaker signal.
Failure received
│
├─ Did the PSP clearly reject the request? → Business Decline → do not retry
│
├─ Did no response arrive at all? → Ambiguous outcome → query status first
│
├─ Is it a 429? → Rate Limited → retry with backoff
│
└─ Is it a 5xx? → Infrastructure → retry, but watch the circuit breaker
Why a single 'just retry' rule fails
A worker that treats every failure the same way fails in two ways: it retries business declines pointlessly (delaying the user experience, sometimes straining card network limits), or it leaves ambiguous outcomes unretried entirely (letting the system forget a payment that actually succeeded). The taxonomy reduces both risks by routing each failure to the right box.
Representing the taxonomy in code
The semantic event's failureReason field should carry one of these four categories, never the provider's raw error text. The provider gateway is responsible for that mapping — an extension of the translator from the previous part.
| failureReason | Retry appropriate | Action |
|---|---|---|
| BusinessDecline | No | Offer the customer another method |
| AmbiguousTimeout | Query first | Check status, then decide |
| RateLimited | Yes | Retry with backoff |
| InfrastructureError | Yes | Retry + watch circuit breaker |
Distinctions that get blurred
❌ Every failure should be retried
✓ A business decline should never be retried; the outcome won't change
❌ Timeout = no error, just retry
✓ Timeout = ambiguity; the real status must be queried first
❌ A 429 is a fault
✓ A 429 is a signal — the system is deliberately slowing you down
Quick comparison across categories
| Category | Outcome known | Retry sensible | Typical cause |
|---|---|---|---|
| Business Decline | Yes | No | Card, balance, fraud |
| Timeout | No | Query first | Network, PSP slowness |
| Rate Limited | Yes | Yes (with wait) | Volume control |
| Infrastructure | Yes | Yes | Provider-side fault |
Checklist for building the taxonomy
- Does every error code the provider returns map explicitly to one of the four categories?
- When an unmapped error code shows up, does the system default to 'query first', or does it blindly retry? (The right default is querying.)
- For timeouts, is a status check actually performed before any retry?
- Does the 429 backoff duration respect the PSP's
Retry-Afterheader when present? - Is the 5xx rate tracked as a metric that can trigger a circuit breaker?
- Does the user flow after a business decline ever accidentally re-enter the retry loop?
What to take away
- 'Failed' isn't one state; it's at least four distinct realities that need four distinct actions.
- Business declines are never retried; timeouts are never blindly retried — they're queried first.
- A 429 is a signal, a 5xx is a fault; both get retried, but with different discipline.
- The taxonomy makes a failure readable from your own
failureReasonenum, not from provider text.
A retry policy written without understanding the failure isn't just useless — it quietly causes harm.
The next part builds the actual retry algorithm for each of these four categories: backoff, jitter, caps, and circuit breakers.
FAQ
Frequently asked questions
What is Business decline?
The PSP rejects the request for a reason tied to the card itself: insufficient funds, suspected fraud.
What is Transient failure?
A temporary fault where retrying the same request makes sense: timeout, 5xx, 429.
Is it true that "Every failure should be retried"?
A business decline should never be retried; the outcome won't change
What does this part lock in?
A declined card, a network timeout, a PSP returning 429, and a PSP returning 500 can all look like 'failure' — yet each demands a completely different response. This part builds a taxonomy that makes those differences visible. 'Failed' isn't one state; it's at least four distinct realities that need four distinct actions. The previous part showed how semantic events like `PaymentFailed` hide the provider's status codes. But a single `PaymentFailed` event still isn't enough, because the word 'failed' covers very different realities.
Engineering Principles Learned
- Failed isn't one state; each category demands a different action.
- An ambiguous outcome is queried, never blindly retried.
- A 429 is a signal, not a fault — it's waited out with discipline.
Continue reading
Continue reading
Next in series
Retry Algorithms for Payment Workers
Exponential backoff, jitter, caps, the difference between retry and defer, and circuit breakers — turning the previous part's taxonomy into working code.
Next in series
Semantic Events Over Raw Provider Payloads
Should the webhook the provider gateway receives reach downstream consumers under the PSP's own event name, or as a semantic event like…
Same series
Provider Abstraction Without Leaking SDKs
How the provider gateway owns the PSP SDK while the checkout orchestrator only ever sees a semantic interface — and why card and wallet flows share a…