The payment provider accepted a request. The response never reached the application. The application tries again.
That is an ordinary network failure with a potentially expensive consequence: a second charge, a duplicate ledger entry, or an invoice marked paid twice. The hard part is that the application does not initially know whether the first attempt failed before or after the provider completed the work.
I treat that uncertainty as a design requirement. A retry needs an identity that survives the failed attempt, and each system needs a clear rule for deciding whether the work has already happened.
Name the operation before sending it
Suppose a hypothetical application is collecting payment for invoice inv_1042. A user clicks Pay, the server submits a request, and the connection times out.
Generating a fresh request identity on retry would describe a new operation to a provider that deduplicates by that identity. Reusing the identity for the original payment attempt lets the provider recognize the retry—subject to that provider’s documented semantics and retention period.
Brandur Leach’s article on idempotent API design (opens in a new tab) explains the ambiguous-outcome problem. Stripe’s current idempotency documentation (opens in a new tab) is the place to verify its actual behavior, including differences between API versions. An architectural principle does not replace the contract of the API being called.
An invoice number alone is not always a sufficient operation key. An invoice may have a failed attempt followed by a genuinely new attempt, or allow multiple legitimate payments. I would create a durable payment-attempt record, associate it with the invoice, and use that attempt’s stable identity for retries of that operation.
The attempt should also preserve the expected amount and currency. An operation identity should not quietly come to mean a different request after someone edits an invoice.
Receiving the event twice is a separate problem
The provider may later notify the application through a webhook. Repeated delivery does not necessarily mean a second payment occurred. It may mean the acknowledgement was lost or processing failed.
Stripe’s webhook documentation (opens in a new tab) explicitly covers duplicate deliveries and events arriving out of order. It distinguishes repeated event IDs from separate events concerning the same underlying object. That distinction matters when choosing a deduplication key.
Keep two identities separate: the event being received and the business operation being recorded. An event ID helps deduplicate delivery. A payment or settlement identity helps prevent different events from producing the same accounting effect twice.
Neither identity should be replaced with “same amount on the same day.” Two customers can legitimately pay identical amounts.
Before accepting an event, verify its signature using the provider’s documented procedure and the original request body. Deduplication establishes whether a message was seen before; it does not establish who sent it.
Put the duplicate check in the database
A handler that first asks “have I processed this?” and then inserts a record can race with another handler doing the same thing. Both can observe that nothing exists. The rule needs an atomic enforcement point.
For an application that records exactly one receipt per provider payment, a PostgreSQL table could express that narrow rule with a unique constraint (opens in a new tab):
CREATE TABLE payment_receipts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
provider text NOT NULL,
account_id text NOT NULL,
payment_id text NOT NULL,
invoice_id text NOT NULL,
amount_minor bigint NOT NULL CHECK (amount_minor > 0),
currency text NOT NULL,
UNIQUE (provider, account_id, payment_id)
);
The account scope matters when the application handles more than one provider account. Integer minor units avoid introducing binary floating-point rounding into this example. Currency still needs validation and an explicit unit convention.
This table is not a complete ledger or a universal payment model. Partial captures, refunds, disputes, and separate settlement movements need their own operation identities and accounting rules. The constraint is appropriate only if its one-receipt-per-payment assumption matches the provider objects being used.
On a conflict, fetch the existing record and compare the expected amount, currency, and invoice association. A matching identity with different financial details should become an exception, not an automatic success.
Decide what a successful acknowledgement promises
Consider this sequence: mark the event processed, write the accounting entry, send an email. A crash between the first two steps can leave an event marked complete with no entry. Reversing the steps without a transaction can create the opposite problem.
Where the receipt, local ledger changes, and processing state share a database, commit the related changes in one transaction (opens in a new tab). For asynchronous processing, acknowledge receipt only after the event is durably accepted for processing. Acceptance and completed business work are different states.
A database transaction cannot make an external email service part of the same atomic commit. One approach is to write an outbox record alongside the local changes and let a worker deliver it later. That worker also needs a retry policy; an outbox alone does not guarantee a recipient will never see a duplicate email.
The same care applies when posting to an external accounting platform. Persist the external reference, use the platform’s supported idempotency mechanisms where available, and reconcile uncertain outcomes before blindly submitting another entry.
Test the uncomfortable sequence
Test a repeated event, two concurrent copies, a crash before commit, a crash after commit but before acknowledgement, and an older event arriving after a newer state. I would also test an existing operation ID paired with an unexpected amount.
For each case, inspect the final records. Counting HTTP success responses is not enough. The questions are whether the intended payment has one appropriate accounting effect, whether unfinished work remains discoverable, and whether an operator can explain what happened.
Retries are essential to recovery. They become dangerous when “try again” is allowed to mean “do the financial work again.” Stable identities, atomic local changes, and visible exceptions make that boundary explicit.


