Imagine a small ordering system. A payment provider sends a message saying an order has been paid. The application records the payment and releases the order. Then the same message arrives again.
If the application treats each message as a new instruction, it may issue two receipts, allocate stock twice, or trigger a second fulfillment. The integration worked on the happy path. It did not have a clear rule for repetition.
This is an illustrative design example, not a description of a client incident. Provider-specific behavior must be checked against the documentation for the service you are integrating.
Ask what identifies the event
An incoming message needs a reliable identity. Depending on the provider, that may be an event identifier, a transaction identifier, or a combination with a defined meaning. Record what each identifier represents; an event and a payment are not always the same thing.
Stripe's webhook documentation explicitly discusses duplicate events and event ordering. It also describes verifying signatures. Those are properties of its interface, not a guarantee about every provider. Use the equivalent primary documentation for your integration. Stripe webhook guidance.
The question for the implementer is simple: “Show me what your system does when it receives the same valid event twice.” A useful demonstration includes the resulting business records, not just two successful HTTP responses.
Make the business change happen once
In the example, the desired state is “payment recorded against this order.” The design should enforce that rule even when two requests arrive close together. Checking a value and then writing it in separate, unprotected steps can leave a race between the requests.
A technical implementation might use a uniqueness constraint and a transaction around the event record and the business-state change. The appropriate method depends on the data store and the provider's event model. Ask the developer to explain the invariant they are protecting: which result must never happen twice?
If fulfillment happens in another system, that boundary also needs a plan. Recording a payment exactly once does not by itself prevent a downstream action from repeating after a retry.
Plan for a missing response
Suppose the sender never receives your acknowledgment. The sender cannot necessarily tell whether your application did nothing or completed the work before the connection failed.
For outgoing requests, an idempotency mechanism can make supported operations safer to retry. Stripe, for instance, lets clients reuse an idempotency key and checks whether the repeated parameters match. That is a documented API feature, not something achieved by attaching an arbitrary header to any service. Stripe idempotent requests.
Write down which operations can be retried, which key they use, and when a person needs to investigate. “Retry everything” can amplify the original problem.
Test the cases that look boring
| Test | Business question |
|---|---|
| Identical event arrives twice | Is there still one payment record? |
| Two copies arrive together | Does the same rule hold under concurrency? |
| Events arrive out of order | Can an older event undo a newer state? |
| Application fails after recording the event | Can unfinished work be recovered? |
| Signature or equivalent verification fails | Is the message rejected without changing business data? |
| Provider is unavailable | Is the customer shown an accurate pending or failed state? |
Run these tests in an appropriate sandbox with synthetic records. Do not experiment against real customer payments.
Give operations a reconciliation route
Even a careful integration needs a way to investigate discrepancies. Agree how an authorized person compares provider records with application records, identifies an unmatched transaction, and resolves it without manually inventing a second payment.
Keep useful correlation identifiers in access-controlled logs. Avoid storing full payment payloads simply because they are available. Decide which details are necessary and how long they are retained.
An integration is ready when repeated, delayed and failed messages have defined outcomes. The successful first request is only the beginning of that conversation.
Have a version of this problem?
Let’s talk it through.
