Skip to content
Payments & Integrations

Making Payment Workflows Reliable

Payment webhooks can arrive late, more than once, or after an ambiguous timeout. This case study explores how durable ingestion, idempotent processing, and explicit recovery paths protect financial correctness at the boundary with external providers.

Status
Past Work
Category
Payments & Integrations
Source
Closed source

Case study

Payment systems depend on external providers whose delivery behavior cannot be fully controlled. Webhooks may arrive more than once, reach the system late, appear out of order, or be retried after the original request has already been processed.

The engineering challenge is not to make those conditions disappear. It is to design the boundary so that they do not compromise financial correctness.

This case study describes a general approach I used when working on backend workflows around payment processing and external provider integrations. Names, providers, internal architecture, and implementation details are intentionally omitted.

The problem

Payment providers use webhooks to report events such as successful captures, refunds, disputes, cancellations, and changes in transaction state.

Those notifications are commonly delivered with at-least-once semantics. A provider may send the same event repeatedly until it receives a successful response. Network timeouts can also create ambiguous situations where the provider considers a delivery unsuccessful even though the receiving system has already accepted it.

A naive webhook handler can therefore produce several classes of failure:

  • the same financial transition is applied more than once;
  • an event is acknowledged before it has been stored safely;
  • provider retries create duplicate internal work;
  • a temporary downstream failure causes the notification to be lost;
  • events are processed in a different order than they were created;
  • the system cannot explain whether a callback was received, processed, retried, rejected, or still waiting;
  • a partially completed workflow leaves internal state inconsistent with the external provider.

In payment systems, these are not minor integration problems. They affect money movement, customer balances, transaction state, and the ability to explain what happened after an incident.

Context

The work existed at the boundary between an internal payment system and external financial providers.

That boundary combined several difficult properties:

  • the provider controlled delivery timing;
  • messages could be duplicated or delayed;
  • network failures created uncertain outcomes;
  • internal workflows could involve several services;
  • downstream operations could fail independently;
  • retries could be necessary but also dangerous;
  • correctness mattered more than immediate consistency.

The objective was to make this behavior explicit. Duplicate delivery, temporary failure, and delayed processing had to be treated as normal operating conditions rather than exceptional edge cases.

Engineering goals

The design focused on a small set of properties.

Preserve every accepted notification

Once the system acknowledged a webhook, the notification had to be stored durably enough to survive a process crash or downstream outage.

Make repeated delivery safe

Receiving the same provider event several times could not apply the same business effect several times.

Separate transport from business processing

The webhook endpoint needed to validate and accept the notification quickly. Complex payment logic, downstream calls, and state transitions should not keep the provider connection open.

Make failure recoverable

Temporary failures needed controlled retries. Persistent failures needed a visible recovery path rather than silent loss.

Keep the processing history explainable

Operators needed to determine what was received, what happened during processing, and why an event reached its current state.

Solution overview

The core design separated webhook ingestion from payment processing.

The synchronous request path performed only the work necessary to verify, identify, and durably record the notification. Business processing happened asynchronously after the provider had received an acknowledgement.

At a high level, the flow was:

  1. receive the provider callback;
  2. verify its authenticity and basic structure;
  3. extract a stable provider event identifier;
  4. persist the notification and its relevant metadata;
  5. acknowledge receipt promptly;
  6. enqueue the event for asynchronous processing;
  7. process the business transition idempotently;
  8. emit internal events for downstream workflows;
  9. retry recoverable failures according to an explicit policy;
  10. surface persistent failures for investigation or controlled replay.

This structure reduced the number of responsibilities inside the HTTP request and created a durable boundary between unreliable external delivery and internal processing.

Architecture

A simplified processing model looked like this:

External provider
        |
        v
Webhook endpoint
        |
        +--> verify authenticity
        |
        +--> identify provider event
        |
        +--> persist notification
        |
        +--> acknowledge receipt
        |
        v
Durable queue
        |
        v
Idempotent payment processor
        |
        +--> apply state transition
        |
        +--> record processing result
        |
        +--> emit internal event
        |
        v
Downstream workflows

The important architectural boundary was not the queue itself. It was the decision to treat receiving a notification and applying its business effect as two separate operations with separate failure behavior.

Idempotency as the central invariant

The design did not depend on preventing duplicate delivery. That would have required control over a provider and network that the system did not have.

Instead, the system made duplicate processing safe.

Each provider event carried, or allowed the derivation of, a stable identifier. That identifier became part of the idempotency boundary for processing.

Before applying a business effect, the processor checked whether the event had already produced a completed result. If it had, the duplicate delivery could be treated as a no-op or return the previously recorded outcome.

The important property was convergence:

Processing the same provider event several times should produce the same business state as processing it once.

This is more useful than relying on a fragile claim of exactly-once delivery. The transport remains at least once, while the business operation becomes idempotent.

Acknowledge quickly, process asynchronously

Performing payment logic directly inside the webhook request creates several risks.

A downstream service may be slow. A database lock may take longer than expected. Another provider call may time out. If the original webhook request remains open during that work, the external provider may retry even though part of the operation has already completed.

The endpoint therefore acknowledged the provider after the notification had been validated and persisted, not after the entire payment workflow had finished.

This created a deliberate trade-off:

  • the provider received a fast and predictable response;
  • internal processing could continue independently;
  • temporary failures no longer depended on the provider sending the event again;
  • users and downstream systems had to tolerate a short period of eventual consistency.

For payment workflows, that trade-off was preferable to coupling external delivery directly to the full internal processing path.

Explicit retry behavior

Retries were useful only when the failure was likely to be temporary and the operation was safe to repeat.

The processing policy distinguished between several outcomes.

Retryable failure

Examples include temporary database unavailability, a short-lived internal service outage, or a recoverable network error.

The event could be retried with bounded backoff and a defined attempt limit.

Non-retryable failure

Examples include invalid payload structure, failed signature verification, an unsupported event type, or a business transition that violated a known invariant.

Retrying would not change the outcome. The event needed to be recorded and surfaced with a clear reason.

Ambiguous failure

Some failures occurred after work may already have been performed. These cases required idempotent checks and reconciliation rather than a blind retry.

The retry policy was therefore part of the correctness model. It was not a generic wrapper added around every exception.

Ordering and state transitions

Webhook ordering could not be assumed.

A provider might deliver a later state before an earlier one, or retry an older event after a newer transition had already been processed.

The internal state model therefore needed to validate transitions instead of trusting arrival order.

Depending on the event type, this could mean:

  • rejecting a stale transition;
  • recording the event without changing current state;
  • reconciling against the provider;
  • waiting for missing context;
  • applying a transition only when its preconditions were satisfied.

This avoided treating queue order as business truth.

Durable processing history

Reliability required more than completing the happy path. The system needed enough information to explain its own behavior.

For each notification, the processing history could include:

  • provider event identifier;
  • event type;
  • time received;
  • verification result;
  • current processing status;
  • number of attempts;
  • last failure classification;
  • time of the next retry;
  • completed business outcome;
  • internal events emitted;
  • reason for rejection or manual review.

This made failures inspectable and reduced dependence on reconstructing events from scattered application logs.

Failure scenarios

The design was evaluated against concrete production scenarios.

The provider sends the same webhook repeatedly

The notification may be recorded more than once at the transport layer, but the idempotent processor applies the business effect once.

The process crashes after the webhook is acknowledged

Because the notification was stored durably before acknowledgement, processing can continue after recovery.

The worker crashes after applying the effect but before marking completion

The next attempt checks the idempotency record or current business state. It does not apply the effect again.

A downstream dependency is temporarily unavailable

The event remains durable and is retried according to policy.

A persistent failure exceeds the retry budget

The event moves into a visible terminal or review state rather than being silently discarded.

Events arrive out of order

The state transition is validated against current domain state instead of being applied solely according to arrival time.

The provider reports a state that conflicts with the internal system

The discrepancy is preserved and routed into reconciliation rather than hidden by an automatic overwrite.

Observability

The most useful signals were tied to the processing lifecycle rather than only to HTTP response codes.

Relevant operational signals included:

  • webhook verification failures;
  • duplicate delivery rate;
  • time between receipt and successful processing;
  • retry attempts by failure category;
  • events waiting beyond an expected processing window;
  • terminal failures requiring review;
  • reconciliation mismatches;
  • queue age and processing lag;
  • downstream event publication failures.

These signals made it possible to distinguish a temporary delivery spike from a systemic processing problem.

A successful 2xx response to the provider did not mean that the business workflow had completed. Observability needed to represent the asynchronous state that followed.

Important engineering decisions

Persist before acknowledging

Acknowledging a notification that exists only in process memory creates a loss window. Durable recording had to happen before returning success to the provider.

Treat duplicate delivery as normal

Duplicate callbacks were not exceptional. The processing model assumed they would happen and made them safe.

Keep idempotency close to the business effect

Deduplicating only at the HTTP boundary was insufficient. A notification could still be replayed internally, or processing could crash midway through a workflow. Idempotency needed to protect the operation that changed business state.

Avoid relying on queue order

Transport order was not a valid substitute for domain transition rules.

Separate retryable and terminal failures

A retry policy that treats every exception the same can create endless loops, provider pressure, and delayed incidents.

Preserve evidence for reconciliation

Financial systems sometimes need to compare internal state with provider state. Processing history and stable identifiers made that possible.

Trade-offs

Eventual consistency

Asynchronous processing meant that a notification could be accepted before its business effect became visible everywhere.

The system needed clear status transitions and downstream consumers that could tolerate this delay.

Additional infrastructure

Durable storage, queues, idempotency records, retry metadata, and operational tooling introduced complexity.

That complexity was justified because it made failure behavior explicit and recoverable. Keeping the handler synchronous would have produced a simpler diagram but a more fragile production system.

Storage of processing history

Keeping enough information for replay and investigation increased storage and retention requirements.

The alternative was losing the ability to explain financial state after an incident.

Idempotency scope

Choosing the wrong key could either allow duplicate effects or incorrectly collapse distinct operations.

Idempotency therefore had to be defined in business terms, using identifiers whose scope matched the operation being protected.

What I would avoid

Several common approaches appear simpler but create fragile behavior.

Doing all work inside the webhook handler

This couples provider delivery to every internal dependency and makes timeouts dangerous.

Treating a unique database constraint as the entire solution

A unique event identifier can prevent inserting the same notification twice, but it does not automatically make downstream business processing idempotent.

Retrying every failure

Some failures are permanent, and some operations have ambiguous outcomes. Blind retries can make the situation worse.

Assuming delivery order

External webhook order should not define valid domain transitions.

Deleting failed events after logging them

A log entry is not a recovery mechanism. Persistent failures need durable state and an explicit operational path.

Lessons

The most important lesson was that exactly-once processing is usually the wrong starting point.

A more practical model is:

at-least-once delivery, durable ingestion, and idempotent business processing.

This accepts the behavior of real networks and external providers instead of designing around an idealized transport guarantee.

The second lesson was that retries, idempotency, ordering, observability, and recovery cannot be designed independently. They form one execution policy at the provider boundary.

When that policy is explicit, duplicate callbacks and temporary failures stop being surprising incidents. They become expected states the system knows how to handle, explain, and recover from.

  • Reliability InfrastructureOpen sourceActive

    RelPrim

    Creator & Maintainer

    RelPrim is an open-source Python reliability layer for operations that cross process, network, or provider boundaries. It brings retries, timeouts, fallbacks, circuit breakers, validation, and execution reporting into one explicit operation model without hiding the domain decisions that make recovery safe.

    • Python
    • Reliability policies
    • Structured execution
  • AI Creative PlatformActive

    Ghostviber

    Founder & Engineer

    Ghostviber is an AI creative platform built around the full songwriting workflow, from the first line to generated audio and artwork. I designed and built the product architecture, multilingual rhyme engine, AI orchestration, credit ledger, asynchronous workflows, storage, and reliability mechanisms needed to turn variable external providers into one coherent user experience.

    • AI orchestration
    • Distributed workflows
    • Usage-based billing

Let's talk systems

Building a system that has to remain predictable under pressure?

I help teams shape the systems their products depend on, especially early on or when reliability becomes critical. I also write and speak about these problems. Tell me what you’re building and where it hurts.