Failure Scenarios & Scaling
How each failure mode manifests, the financial risk it carries, how it's detected, how the system behaves safely in the moment, how it recovers, and how it's prevented going forward -- from duplicate requests and provider timeouts through webhook edge cases, event delivery, reconciliation, refunds, the ledger, traffic spikes and regional failure.
Duplicate Request
The same payment request arrives twice -- a double-click, a client retry, or at-least-once delivery from a message broker. There are several distinct variants, and each resolves differently.
Duplicate after completion
The second request carries the same idempotency key as the first, which already completed. Resolve it against the durable idempotency record and return the same result -- no second provider call.
Concurrent duplicate while the first is still in progress
The second request finds an in-progress record for the same key. It resolves to the same logical payment and returns/waits for the current state -- it does not need to block on a distributed lock, and it must not proceed to an independent provider call.
Same key, different amount
Reject as an idempotency-key conflict. Do not silently execute the new amount and do not silently return the old result -- both would misrepresent what the caller actually asked for.
Duplicate provider call within one attempt
If the same attempt genuinely needs to be retried at the provider, reuse that attempt's provider idempotency key rather than generating a new one.
Provider Timeout
The provider processes the payment but the response is lost. Internal state is unresolved. Provider state may already be SUCCESS.
Failure
A request to the provider times out or the connection drops after the request was sent.
Financial risk
A blind retry with a new provider identity can double-charge if the original request actually succeeded; blindly marking it FAILED can cause a missed charge to go undetected if the provider actually failed and the customer needed to be informed.
Safe behavior
Do not mark it FAILED. Do not generate a fresh, independently-identified attempt. Resolve it via the ambiguous-outcome decision tree: same-identity retry if the provider documents it as safe, status query by stable reference, await webhook, or hand off to reconciliation.
Recovery
Resolution typically completes once one of the above channels reports a definitive outcome -- how long that takes is provider- and operation-specific, not a fixed universal window.
Prevention
Use a provider whose API supports a status-query-by-reference endpoint wherever possible, so ambiguity resolution doesn't depend on webhooks alone.
Delayed Webhook
The provider's webhook is delayed, duplicated, arrives out of order, or arrives for a payment already resolved by another channel. All of these are normal operating conditions the webhook handler must tolerate, not rare edge cases.
Delayed delivery
PROCESSING is a valid, non-error intermediate state while a webhook is in flight. The payment is not resubmitted. If the delay exceeds the reconciliation threshold, reconciliation queries the provider directly rather than waiting indefinitely.
Duplicate delivery
The same webhook event arrives more than once. Deduplicate on the provider's event/delivery ID -- reprocessing an already-applied event is a safe no-op.
Out-of-order delivery
An older event (e.g. PROCESSING) arrives after a newer one (e.g. SUCCESS) was already applied. The guarded transition table rejects the invalid SUCCESS -> PROCESSING regression.
Arrives after reconciliation or a refund already resolved it
The transition table and idempotent processing apply equally here -- an event proposing a transition that's no longer valid given the payment's current state is rejected, not blindly applied.
Database Failure
The payment database becomes unavailable. Payment state cannot be persisted. The system must stop rather than risk incorrect state -- and the actual recoverability of a resulting gap is more nuanced than 'unrecoverable.'
DB unavailable before payment creation
Reject cleanly -- no provider call, a clear error to the client. This is the one absolute rule: never call the provider before a durable local identity exists for the attempt.
DB failure after provider success, before local commit
Highly undesirable, but not automatically unrecoverable -- if a stable merchant reference or provider idempotency identity was sent to the provider before the crash, the provider's own records, a subsequent webhook, or financial settlement reconciliation may allow recovery. See Failure & Scale -> Crash After Provider Success Before Local Commit for the full walkthrough.
DB commit succeeds but the response to the service/caller is lost
The durable idempotency record already reflects the correct outcome -- a client retry with the same key resolves to it rather than creating a new charge.
Failover to a stale replica
A read from a lagging replica must never be used to decide whether to charge -- only the authoritative, sufficiently fresh store can make that call. See Trade-offs & Decisions -> Single Database vs Sharded Payment Store.
Crash After Provider Success Before Local Commit
This is the canonical payment failure: the provider actually processed and succeeded, but the service crashed before durably recording that fact.
Failure
The provider charges the customer and returns success, but the service process crashes before writing SUCCESS (or even before writing PROCESSING) to the local database.
Financial risk
The provider believes the charge succeeded. If the local system loses track of the attempt entirely, a client retry with a fresh identity could produce a second charge -- or the successful charge could go completely unrecorded.
Detection
A stable provider idempotency/reference identity, generated before the provider call, is what makes this detectable -- either via the provider's webhook, a status query against that reference, or reconciliation against provider records.
Safe behavior
The retry path resolves via the same provider idempotency identity, not a new one. The local state remains PROCESSING/unresolved until one of the resolution channels reports the true outcome -- it is never blindly marked FAILED or blindly assumed SUCCESS.
Recovery
Webhook, status query, or reconciliation eventually reports the true outcome and the local record is updated through a guarded transition.
Prevention
Always create the durable local attempt record with its provider idempotency identity before calling the provider -- so recovery has something stable to query against, even if the write of the result is what got lost.
Crash After Local SUCCESS Before API Response
The service durably records SUCCESS, but the process crashes (or the network drops) before the success response reaches the client.
Failure
Local state is durably SUCCESS. The client, having seen no response, experiences this exactly like any other timeout.
Financial risk
If the client's retry is treated as a brand-new request, it risks a second charge -- even though the original charge already succeeded and is durably recorded.
Detection
The client retries using the same idempotency key.
Safe behavior
The durable idempotency record resolves the retry to the existing, already-SUCCEEDED payment and returns that result -- no new provider call is made.
Recovery
No recovery action needed beyond the idempotency resolution itself -- this is exactly the case durable idempotency is designed to make a non-event.
Prevention
Ensure the client always retries with the same idempotency key it used originally, and that the durable record (not a cache) is what the retry path checks first.
Idempotency Store/Redis Failure
An optional idempotency cache goes down. Payment correctness must not depend on it being up.
Failure
Redis (or an equivalent acceleration cache) used in front of the durable idempotency record becomes unavailable.
Financial risk
None, if the design is correct -- the risk only appears if the cache was mistakenly treated as the authoritative source of truth.
Detection
Cache health checks / connection errors from the idempotency lookup path.
Safe behavior
Fall back to the durable database uniqueness check, and never fall through to allowing a duplicate provider call. The database check is slower without the cache, but it is still correct.
Recovery
Once the cache recovers, it resumes accelerating lookups; no data reconciliation is needed because the cache was never the source of truth.
Prevention
Never design the idempotency path such that a cache outage removes the durable database check -- the cache should only ever be a read-through accelerator in front of it.
Same Idempotency Key, Different Payload
A client reuses an idempotency key with a materially different request -- most often a client-side bug, occasionally something worth investigating further.
Failure
A request arrives with an idempotency key that already maps to an existing payment, but the amount, currency, or business reference differs from the original request's stored fingerprint.
Financial risk
Silently executing the new request would create an unexpected charge under a key the client believes maps to the original request. Silently returning the old result would silently ignore a request the client may have genuinely intended to be different.
Detection
Comparing the incoming request's fingerprint against the stored fingerprint for that key.
Safe behavior
Reject with an explicit idempotency-key conflict error -- do not execute the new parameters and do not silently return the stale result.
Recovery
The client is expected to generate a new idempotency key for what is, in fact, a new logical payment.
Prevention
Track idempotency-key-conflict rate as an observability metric -- a sustained rate often indicates a client-side bug generating non-unique keys.
Webhook Duplicate
The provider delivers the same webhook event more than once -- expected behavior under at-least-once delivery, not a bug on the provider's side.
Failure
The same provider event (same event/delivery ID) is delivered two or more times.
Financial risk
None, if deduplicated correctly. If the handler blindly reapplies every delivery, a duplicate SUCCESS event could double-post to the ledger or re-trigger downstream effects.
Detection
Deduplicate on the provider's event/delivery ID, recorded at persistence time.
Safe behavior
The second and subsequent deliveries are recognized as already-processed and are acknowledged without reapplying the business effect.
Recovery
None needed -- this is the expected steady-state behavior of a correctly built handler.
Prevention
Persist the event ID durably before ACKing, and always check it before applying any state change or side effect.
Webhook Out of Order
Webhook delivery order does not always match the order events actually occurred at the provider.
Failure
An older event (e.g. PROCESSING, generated before a SUCCESS event) is delivered after the newer SUCCESS event has already been applied.
Financial risk
Applying the stale event naively would regress a completed payment back to a non-terminal state, potentially re-triggering downstream effects that assumed the payment was done.
Detection
The guarded transition table recognizes SUCCESS -> PROCESSING as an invalid transition for the current state; a reliable provider sequence/version number, where available, gives an additional signal.
Safe behavior
The stale event is rejected/ignored rather than applied -- the payment's current state is left untouched.
Recovery
None needed beyond logging the rejected transition for observability.
Prevention
Never apply a webhook event without checking it against the current state's valid-transition table first; use provider sequence/version fields when the provider offers them.
Invalid/Forged Webhook
A request arrives at the webhook endpoint claiming to be from the provider, but fails signature verification -- possibly a misconfiguration, possibly a malicious attempt to forge a payment success.
Failure
An inbound webhook request fails signature/MAC verification, timestamp/replay-window verification, or endpoint secret verification.
Financial risk
If processed anyway, a forged webhook could mark a payment as successfully charged when no money actually moved, or falsely mark a legitimate payment as failed/refunded.
Detection
Signature/MAC check against the raw request body, replay-window check, and endpoint secret verification performed before any parsing of business fields.
Safe behavior
Reject before any financial state mutation. Never rely on the parsed payload's own claims (e.g. a 'signature' field inside the JSON body) as a substitute for verifying the actual transport-level signature.
Recovery
Log and alert on verification failures; investigate whether it reflects a misconfigured secret (operational) or an actual forgery attempt (security incident).
Prevention
Rotate webhook signing secrets on a defined schedule, and verify against the exact raw bytes the provider signed -- not a re-serialized version of the parsed payload.
DB SUCCESS, Event Publish Fails
The database durably records SUCCESS, but publishing the corresponding event to the broker fails or is never attempted before a crash.
Failure
A direct (non-outbox) publish call after the DB commit fails, times out, or is never reached because the process crashed between the commit and the publish call.
Financial risk
Downstream consumers (order fulfillment, notifications, ledger posting) never receive the event -- the payment succeeded but nothing downstream happens.
Detection
Outbox lag monitoring (rows written but not yet marked published) surfaces this directly if using the outbox pattern; without an outbox, this may only surface when a customer notices a missing order.
Safe behavior
Use a transactional outbox so the event's existence is guaranteed by the same transaction that committed the payment state -- the publish gap becomes a relay-lag problem to monitor, rather than a silent event loss.
Recovery
The outbox relay retries publishing the unpublished row until the broker acknowledges it.
Prevention
Do not rely on a direct publish-after-commit call for anything downstream treats as consequential -- use the transactional outbox pattern. See Architecture Deep Dive -> Transactional Outbox.
Duplicate Downstream Event
The outbox relay (or any at-least-once publisher) delivers the same payment event to a consumer more than once -- an accepted trade-off of guaranteeing the event isn't lost.
Failure
A consumer receives the same payment.success (or similar) event twice.
Financial risk
If the consumer isn't idempotent, order fulfillment could ship twice, a notification could send twice, or a ledger entry could post twice.
Detection
The event carries a stable event_id/payment_id/business-operation identity the consumer can check against its own already-processed record.
Safe behavior
Each consumer deduplicates or makes its operation idempotent using that stable identity before applying its effect.
Recovery
No system-level recovery needed -- this is the expected behavior of a correctly built consumer under at-least-once delivery.
Prevention
Design every downstream consumer's operation to be idempotent from the start, keyed on event_id or the equivalent business operation identity -- not just the payment consumer, every consumer.
Reconciliation Job Failure
Reconciliation is itself a piece of software that runs against external dependencies -- it can crash, repeat a batch, or get rate-limited mid-run.
Failure
The reconciliation job crashes partway through a batch, is restarted and re-runs the same batch, or hits provider rate limits mid-sweep.
Financial risk
A naively re-run batch could double-create ledger entries or issue duplicate refund repairs if repairs aren't themselves idempotent.
Detection
Reconciliation job monitoring: run duration, completion status, repair counts, and a stalled/failed-run alert.
Safe behavior
Repairs use stable, idempotent reconciliation/action identities, so a re-run of an already-completed (or partially-completed) batch recognizes prior repairs and skips them rather than reapplying them.
Recovery
Checkpoints/watermarks let a restarted job resume from where it left off rather than restarting the entire sweep; provider rate limits are respected via backoff within the job itself.
Prevention
Build reconciliation with the same idempotency discipline as the rest of the system from the start -- it is not exempt from the rules it enforces on everything else.
Provider/Internal State Conflict
The internal record and the provider's own record disagree -- and the resolution is not simply 'whichever was updated most recently.'
Failure
Examples: internal state is FAILED but the provider shows SUCCESS; internal state is SUCCESS but the provider now shows the transaction reversed; the amount recorded internally doesn't match the provider's amount.
Financial risk
Trusting the wrong side of the conflict can mean treating a real charge as if it never happened (missed revenue tracking, no ledger entry) or treating a reversed/failed charge as if it still succeeded (incorrect fulfillment, incorrect customer-facing status).
Detection
Reconciliation's comparison step, or a status query triggered by unexpected divergence.
Safe behavior
Do not resolve by blindly picking the state with the latest timestamp. Use the provider's own semantics for what the conflicting state actually means (e.g. a reversal is authoritative once confirmed), combined with financial reconciliation against settlement data where available.
Recovery
The resolution updates internal state through a guarded transition and logs the conflict and its resolution to the audit log; if money movement is involved, the ledger reflects the correction as its own entry, not a silent edit to the original.
Prevention
Track provider/internal mismatch rate as an observability metric -- a sustained rate is a leading indicator of a systemic integration issue, not just isolated incidents.
Multi-Provider Failover Double Charge
One of the most important multi-provider failure cases: Provider A times out, the router sends the same logical payment to Provider B, and both providers actually succeed.
Failure
A request to Provider A times out without a definitive response. The router treats this as a failure and immediately routes the same logical payment to Provider B, which succeeds. Provider A had, in fact, also processed the charge successfully before timing out.
Financial risk
The customer is charged twice, once at each provider -- and because they're different providers, neither has visibility into the other's transaction.
Detection
Reconciliation comparing payment attempts across providers for the same logical payment; a customer or support report of a double charge.
Safe behavior
This is prevented at the routing decision, not detected afterward: only fail over to a different provider when the first attempt is known not to have created a charge (a definitive failure), or when there's an explicit, safe cross-provider orchestration/reconciliation mechanism designed for this specific case.
Recovery
If it still occurs, resolution requires a refund at one of the two providers, tied together by a reconciliation record linking both attempts to the one logical payment.
Prevention
Maintain separate payment attempts per provider under one logical payment (see Architecture Deep Dive -> Payment Intent and Attempt Model), and never treat an ambiguous timeout as equivalent to a definitive failure when deciding whether to fail over.
Refund Timeout
A refund request to the provider times out -- the exact same ambiguous-outcome problem as a charge, applied to money moving in the other direction.
Failure
A refund request is sent to the provider; the response is lost or the connection drops before a definitive result is received.
Financial risk
A blind retry with a new refund identity risks a duplicate refund; treating it as failed and never following up risks a refund the customer is owed silently never happening.
Detection
The refund's own durable state remains unresolved, discoverable via the same reconciliation sweep used for charges.
Safe behavior
Resolve exactly like a charge timeout: reuse the same refund idempotency/provider identity for any safe retry, query refund status by stable reference, await the refund webhook, or reconcile.
Recovery
One of the above channels eventually confirms the refund's true outcome, and the total-refunded invariant is checked before finalizing.
Prevention
Give every refund its own idempotency key and provider reference from the start, exactly as a charge attempt has -- do not treat a refund as a simpler, ambiguity-free operation.
Concurrent Partial Refunds
Two operators or two services independently issue partial refunds against the same payment at nearly the same time.
Failure
Two refund requests for the same payment are submitted concurrently, each individually valid against the captured amount, but not valid together.
Financial risk
Without a transactional check against the running refunded total, both can succeed independently and their combined total can exceed the captured amount.
Detection
The database constraint/transactional check itself is the detection mechanism -- it should fail the second request at write time, not after the fact.
Safe behavior
Enforce sum(successful refunds) <= captured amount transactionally, using a conditional check against the current running total within the same transaction that records the new refund -- not a check-then-write pattern with a gap between them.
Recovery
The second, over-limit refund request is rejected outright rather than needing to be reversed after the fact.
Prevention
Never validate a refund amount against a previously-read total without also transactionally re-checking it at write time -- read-then-write without a transactional guard is exactly the race this scenario describes.
Ledger Posting Failure
A payment reaches SUCCESS, but the corresponding financial ledger entry is missing -- a gap between operational state and the financial record of what happened.
Failure
The payment state transitions to SUCCESS, but the ledger-posting consumer fails to write the corresponding entry, or never receives the event.
Financial risk
Financial records under-represent actual money movement -- accounting and settlement reconciliation will eventually disagree with the operational payment state.
Detection
Ledger-posting lag/backlog monitoring, and financial reconciliation comparing the ledger against provider settlement data.
Safe behavior
Order fulfillment and other downstream effects should not be made to depend on the ledger posting having already happened -- they depend on the payment's operational SUCCESS state, which is already durable. The ledger gap is repaired independently.
Recovery
Depending on architecture: the transactional outbox retries delivery to the ledger consumer, or reconciliation detects the missing entry and posts the correction with a stable, idempotent action identity.
Prevention
Treat the ledger-posting consumer with the same idempotent, at-least-once-aware design as every other downstream consumer -- and never make fulfillment logic implicitly depend on an unmonitored, untracked financial posting having already succeeded.
Traffic Spike
A sudden surge in payment volume -- a flash sale, a marketing push -- stresses every layer of the system simultaneously.
Failure
Payment creation rate, webhook volume, and downstream event volume all spike well beyond normal levels at once.
Financial risk
None directly from the spike itself, provided correctness mechanisms hold under load -- the risk is that correctness gets silently traded away under pressure.
Detection
API latency/error rate, database write contention, provider rate-limit responses, webhook processing lag, outbox/queue backlog growth, and reconciliation lag, tracked as a set rather than any single metric.
Safe behavior
Scale the stateless API tier horizontally; let database write contention and provider rate limits set the real throughput ceiling rather than trying to force more throughput than the true bottleneck allows; let the queue/outbox absorb temporary backlog within its retention window.
Recovery
Backlogs (webhook processing, outbox publishing, reconciliation) drain once the spike subsides, provided their retention/backlog limits weren't exceeded.
Prevention
Load-test against realistic burst shapes ahead of known high-traffic events, and ensure idempotency and constraint checks are never bypassed as a load-shedding measure -- correctness guarantees are exactly what must not be sacrificed for throughput.
Illustrative numeric trace (planning numbers, not measurements of any real system; they reuse the Complete Design's sizing). Assumptions: normal peak 580 payments/s; the flash sale offers 2,000 payments/s for 300 s (about 3.4× normal peak); each accepted payment emits 2 outbox events; the outbox relay publishes at most 2,500 events/s; the provider limit of 1,500 requests/s is the binding ceiling (the database's ≈2,500 payments/s is not reached).
| Step | Calculation | Result |
|---|---|---|
| Accepted payment rate | min(2,000 offered, 1,500 provider limit) | 1,500 payments/s (the excess is handled as in Provider Rate Limit) |
| Database row writes | 1,500 × 8 | ≈ 12,000 rows/s, below the assumed 20,000 rows/s ceiling |
| Outbox production vs relay capacity | 1,500 × 2 = 3,000 events/s vs 2,500 events/s | backlog grows 500 events/s |
| Backlog at end of spike | 500 × 300 | 150,000 unpublished outbox rows |
| Drain after spike | 150,000 ÷ (2,500 − 580 × 2) | ≈ 112 s, with load back at normal peak |
The takeaway is where the backlog lives: the outbox rows sit in the payment database, so the 150,000-event backlog is a relay-lag number to alert on, not lost events. The correctness path (durable idempotency record, guarded transitions, ledger writes) is unchanged while the downstream views lag by about two minutes.
Provider Rate Limit
The payment provider begins rate-limiting requests -- a distinct failure mode from an outright outage, and one that calls for a different response.
Failure
The provider returns rate-limit responses (e.g. 429s), often with a Retry-After header, once request volume crosses a threshold on their side.
Financial risk
Ignoring the signal and retrying aggressively risks getting further throttled or temporarily blocked, worsening the very problem being retried around.
Detection
Rate-limit response rate, tracked per provider, separate from other error categories.
Safe behavior
Respect the provider's Retry-After guidance; apply bounded concurrency and a client-side rate limiter tuned to the provider's documented limits; apply backpressure or queuing where the operation's semantics allow a delay.
Recovery
Throughput to that provider recovers once request volume drops back under the provider's limit or the rate-limit window elapses.
Prevention
Use a provider-specific circuit/bulkhead so rate limiting on one provider doesn't starve capacity meant for others, and size outbound concurrency against the provider's documented limits up front rather than discovering them under load.
Illustrative numeric trace (same assumptions as the Traffic Spike trace). The provider allows 1,500 requests/s; a spike offers 2,000 payments/s for 300 s; the client-side limiter admits 1,500/s; the wait queue is capped at 30 s of provider throughput (30 × 1,500 = 45,000 payments) so no caller waits longer than about 30 s; beyond the cap, requests are shed with a retry-after response and the client retries with the same idempotency key.
| Step | Calculation | Result |
|---|---|---|
| Excess over the limit | 2,000 − 1,500 | 500 payments/s |
| Time to fill the 45,000-payment queue | 45,000 ÷ 500 | 90 s |
| Shed after the queue fills | 500 × (300 − 90) | 105,000 payments told to retry |
| Provider calls over the spike | 1,500 × 300 | 450,000 |
| Reconciliation of the 600,000 offered | 450,000 sent + 45,000 still queued + 105,000 shed | 600,000 offered |
| Queue drain after the spike | 45,000 ÷ (1,500 − 580) | ≈ 49 s |
| Without the cap, queue at end of spike | 500 × 300 = 150,000; ÷ 1,500 | ≈ 100 s of wait for the last caller |
Retrying at the full offered rate instead would send 2,000 requests/s into a 1,500/s limit and keep the provider throttling; the limiter keeps traffic at the limit and shifts the excess into a bounded wait or an explicit retry. Shed requests are safe only because the idempotency key makes the retry the same logical payment.
Regional Failure
An entire region hosting part of the payment system fails -- the hardest scenario in the module, because nearly every other concept in this design has a regional-failover implication.
Failure
A region hosting the payment API, database, or webhook ingestion becomes unavailable.
Financial risk
Ambiguous in-flight attempts during the failover window carry the same double-charge risk as any other ambiguous outcome, now compounded by the question of which region has write authority for a given payment.
Detection
Regional health checks and failover automation; RPO/RTO targets defined ahead of time for the payment database and ledger.
Safe behavior
Define, before any failure occurs: which region has payment write authority for a given payment (and how idempotency uniqueness is preserved across regions so the same key can't be double-honored in two regions); which region owns a given in-flight provider attempt; how webhook routing reaches the newly-active region; and how the ledger's replication guarantees bound acceptable data loss.
Recovery
Failover promotes a standby region using the same fencing discipline as any single-leader failover, so the original region cannot resume writing after recovering unexpectedly; jobs/attempts in flight during the window are resolved via the same ambiguous-outcome and reconciliation mechanisms used for any other unresolved attempt.
Prevention
Design idempotency uniqueness, provider attempt ownership, and ledger replication with regional failover in mind from the start, rather than treating multi-region as a bolt-on -- and rehearse the failover path before it's needed for real.
Triage summary: scenario, detection, recovery
| Scenario | Detection signal | Recovery or safe response |
|---|---|---|
| Duplicate Request | Idempotency-key lookup; fingerprint mismatch | Return the stored result from the durable idempotency record; reject a different payload |
| Provider Timeout | Attempt left unresolved (PROCESSING) | Ambiguous-outcome tree: same-identity retry, status query, webhook or reconciliation; never mark FAILED |
| Delayed Webhook | Delay beyond the reconciliation threshold | PROCESSING stays valid; reconciliation queries the provider directly |
| Provider Unavailable | Connection errors and 5xx rate | Fail fast; retry definitive failures; breaker stops pile-on; ambiguous in-flight ops go to reconciliation |
| Database Failure | DB errors before or after the provider call | Never call the provider without a durable identity; recover via provider reference, webhook or settlement reconciliation |
| Crash After Provider Success Before Local Commit | Stable provider reference sent before the call | Resolve via webhook, status query or reconciliation through a guarded transition |
| Crash After Local SUCCESS Before API Response | Client retries with the same key | Durable idempotency record returns the SUCCEEDED payment; no new provider call |
| Idempotency Store/Redis Failure | Cache health checks and connection errors | Fall back to the database uniqueness check; no data repair needed |
| Same Idempotency Key, Different Payload | Stored vs incoming request fingerprint | Reject with a key-conflict error; client uses a new key |
| Webhook Duplicate | Provider event/delivery ID seen before | Acknowledge without reapplying the effect |
| Webhook Out of Order | Guarded transition table rejects the regression | Ignore the stale event and log it |
| Invalid/Forged Webhook | Signature, replay-window and secret checks | Reject before any state mutation; alert and investigate |
| DB SUCCESS, Event Publish Fails | Outbox lag (written, unpublished rows) | Relay retries until the broker acknowledges |
| Duplicate Downstream Event | Consumer checks stable event identity | Consumers deduplicate or are idempotent; no system recovery |
| Reconciliation Job Failure | Run duration, completion status, repair counts | Idempotent repair identities and checkpoints let a re-run resume |
| Provider/Internal State Conflict | Reconciliation comparison or status query | Apply provider semantics via a guarded transition; ledger correction as its own entry |
| Multi-Provider Failover Double Charge | Cross-provider attempt comparison; support report | Prevent at routing (fail over only on definitive failure); otherwise refund one charge and link both attempts |
| Refund Timeout | Refund state unresolved in the reconciliation sweep | Same refund identity for retry, status query or webhook; check total-refunded invariant |
| Concurrent Partial Refunds | Constraint or conditional check fails the write | Transactional sum(refunds) <= captured check; reject the over-limit refund |
| Ledger Posting Failure | Ledger lag; ledger vs settlement reconciliation | Outbox retry or reconciliation posts the missing entry idempotently |
| Traffic Spike | Latency, DB contention, backlog and lag tracked together | Scale the API tier; let the true bottleneck set throughput; backlogs drain after the spike |
| Provider Rate Limit | Rate-limit response rate per provider | Honor Retry-After; bounded concurrency and limiter; per-provider bulkhead |
| Regional Failure | Regional health checks; predefined RPO/RTO | Fenced failover to the standby region; in-flight attempts resolved via reconciliation |
- 01Never turn an ambiguous provider outcome into FAILED or a fresh attempt; resolve it with the same provider identity through status query, webhook or reconciliation.
- 02Idempotency correctness lives in the database uniqueness constraint; a cache or lock failure must fall back to it, never fall through to a second provider call.
- 03Webhooks and downstream events are at-least-once: dedupe on event ID and let the guarded transition table reject stale or out-of-order events.
- 04The outbox turns event loss into a measurable relay lag; in the illustrative spike trace a 150,000-event backlog drains in about two minutes without losing any event.
- 05Under a provider rate limit, cap concurrency at the provider's limit and bound the wait queue; shed with a retry-after and rely on the idempotency key for safe client retries.
- 06Money-moving invariants (refund total, ledger entries, regional write authority) are enforced transactionally or by fencing at write time, not detected afterwards.