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.

Distributed payment engine architecture diagram

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

  1. Does every error code the provider returns map explicitly to one of the four categories?
  2. When an unmapped error code shows up, does the system default to 'query first', or does it blindly retry? (The right default is querying.)
  3. For timeouts, is a status check actually performed before any retry?
  4. Does the 429 backoff duration respect the PSP's Retry-After header when present?
  5. Is the 5xx rate tracked as a metric that can trigger a circuit breaker?
  6. Does the user flow after a business decline ever accidentally re-enter the retry loop?

What to take away

  1. 'Failed' isn't one state; it's at least four distinct realities that need four distinct actions.
  2. Business declines are never retried; timeouts are never blindly retried — they're queried first.
  3. A 429 is a signal, a 5xx is a fault; both get retried, but with different discipline.
  4. The taxonomy makes a failure readable from your own failureReason enum, 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

ESSAY

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

Same series

Paylaş