Skip to content
SeeItFlow
System Design · Failure Scenarios & Scale · ~22 min read

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.

Mangesh MandlikLast updated:
Scenario 01

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.

Scenario 02

Provider Timeout

The provider processes the payment but the response is lost. Internal state is unresolved. Provider state may already be SUCCESS.

Swipe to pan
Timeout AmbiguityProvider succeeds — response lost — internal state: UNKNOWNPayment SvcUNKNOWNProviderSUCCESS ✓✗ response loststate = UNKNOWNstate = SUCCESSA timeout ≠ payment failedResolution: webhook or reconciliation
The same ambiguous-outcome mechanics as the architecture walkthrough: the provider may have already returned SUCCESS even though the response never reached the local system -- resolve via the ambiguous-outcome decision tree, never a blind retry or blind FAILED.

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.

Scenario 03

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.

Scenario 04

Provider Unavailable

The provider becomes unavailable. New payments cannot complete. The system must fail gracefully and honestly -- and must distinguish where in the request lifecycle the failure actually occurred.

Connection failure before the request was sent

No ambiguity -- the operation definitively did not reach the provider. Safe to fail immediately or retry the whole operation.

Definitive provider 5xx before the operation was accepted

The provider explicitly rejected or failed to accept the operation -- treat as a transient technical failure eligible for retry per the retry policy, not as an ambiguous outcome.

Timeout after the request may have been accepted

This is the ambiguous case -- see Provider Timeout above. A circuit breaker prevents new traffic from piling onto a failing provider, but it cannot resolve whether operations already in flight when it opened actually succeeded.

Scenario 05

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.

Scenario 06

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.

Scenario 07

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.

Scenario 08

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.

Scenario 09

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.

Scenario 10

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.

Scenario 11

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.

Scenario 12

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.

Scenario 13

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.

Scenario 14

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.

Scenario 15

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.

Scenario 16

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.

Scenario 17

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.

Scenario 18

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.

Scenario 19

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.

Scenario 20

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.

Scenario 21

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).

StepCalculationResult
Accepted payment ratemin(2,000 offered, 1,500 provider limit)1,500 payments/s (the excess is handled as in Provider Rate Limit)
Database row writes1,500 × 8≈ 12,000 rows/s, below the assumed 20,000 rows/s ceiling
Outbox production vs relay capacity1,500 × 2 = 3,000 events/s vs 2,500 events/sbacklog grows 500 events/s
Backlog at end of spike500 × 300150,000 unpublished outbox rows
Drain after spike150,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.

Scenario 22

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.

StepCalculationResult
Excess over the limit2,000 − 1,500500 payments/s
Time to fill the 45,000-payment queue45,000 ÷ 50090 s
Shed after the queue fills500 × (300 − 90)105,000 payments told to retry
Provider calls over the spike1,500 × 300450,000
Reconciliation of the 600,000 offered450,000 sent + 45,000 still queued + 105,000 shed600,000 offered
Queue drain after the spike45,000 ÷ (1,500 − 580)≈ 49 s
Without the cap, queue at end of spike500 × 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.

Scenario 23

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

ScenarioDetection signalRecovery or safe response
Duplicate RequestIdempotency-key lookup; fingerprint mismatchReturn the stored result from the durable idempotency record; reject a different payload
Provider TimeoutAttempt left unresolved (PROCESSING)Ambiguous-outcome tree: same-identity retry, status query, webhook or reconciliation; never mark FAILED
Delayed WebhookDelay beyond the reconciliation thresholdPROCESSING stays valid; reconciliation queries the provider directly
Provider UnavailableConnection errors and 5xx rateFail fast; retry definitive failures; breaker stops pile-on; ambiguous in-flight ops go to reconciliation
Database FailureDB errors before or after the provider callNever call the provider without a durable identity; recover via provider reference, webhook or settlement reconciliation
Crash After Provider Success Before Local CommitStable provider reference sent before the callResolve via webhook, status query or reconciliation through a guarded transition
Crash After Local SUCCESS Before API ResponseClient retries with the same keyDurable idempotency record returns the SUCCEEDED payment; no new provider call
Idempotency Store/Redis FailureCache health checks and connection errorsFall back to the database uniqueness check; no data repair needed
Same Idempotency Key, Different PayloadStored vs incoming request fingerprintReject with a key-conflict error; client uses a new key
Webhook DuplicateProvider event/delivery ID seen beforeAcknowledge without reapplying the effect
Webhook Out of OrderGuarded transition table rejects the regressionIgnore the stale event and log it
Invalid/Forged WebhookSignature, replay-window and secret checksReject before any state mutation; alert and investigate
DB SUCCESS, Event Publish FailsOutbox lag (written, unpublished rows)Relay retries until the broker acknowledges
Duplicate Downstream EventConsumer checks stable event identityConsumers deduplicate or are idempotent; no system recovery
Reconciliation Job FailureRun duration, completion status, repair countsIdempotent repair identities and checkpoints let a re-run resume
Provider/Internal State ConflictReconciliation comparison or status queryApply provider semantics via a guarded transition; ledger correction as its own entry
Multi-Provider Failover Double ChargeCross-provider attempt comparison; support reportPrevent at routing (fail over only on definitive failure); otherwise refund one charge and link both attempts
Refund TimeoutRefund state unresolved in the reconciliation sweepSame refund identity for retry, status query or webhook; check total-refunded invariant
Concurrent Partial RefundsConstraint or conditional check fails the writeTransactional sum(refunds) <= captured check; reject the over-limit refund
Ledger Posting FailureLedger lag; ledger vs settlement reconciliationOutbox retry or reconciliation posts the missing entry idempotently
Traffic SpikeLatency, DB contention, backlog and lag tracked togetherScale the API tier; let the true bottleneck set throughput; backlogs drain after the spike
Provider Rate LimitRate-limit response rate per providerHonor Retry-After; bounded concurrency and limiter; per-provider bulkhead
Regional FailureRegional health checks; predefined RPO/RTOFenced failover to the standby region; in-flight attempts resolved via reconciliation
Key takeaways
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 06Money-moving invariants (refund total, ledger entries, regional write authority) are enforced transactionally or by fencing at write time, not detected afterwards.