Last updated
Idempotency#
Retry uncertain requests safely, preserve operation identity, and reconcile outcomes before creating another payment.
One intent, one reference#
Idempotency allows the same operation to be submitted more than once without producing multiple effects. It matters whenever a timeout, browser refresh, delayed provider response, or retry can repeat a request that moves money or creates a resource.
Create an operation reference when the business intent is recorded, persist it before making the first request, and reuse it for every retry of that same intent. A retry must not silently generate a new reference.
Recognise an uncertain outcome#
A client timeout means that the client stopped waiting. It does not prove that the server cancelled processing. The server or its provider may already have accepted or completed the operation.
| Observation | What it establishes | Safe next action |
|---|---|---|
| Local validation fails before sending | No upstream request was sent. | Correct the input and submit the intended operation. |
| Explicit rejected response | The documented rejection occurred. | Follow the backend error guidance; confirm whether an effect occurred. |
| Timeout or connection loss after sending | The outcome is unknown. | Check status using the saved reference before another write. |
| Pending response | Processing remains unresolved. | Poll through the approved status endpoint or await a verified webhook. |
| Duplicate reference response | The reference already exists or has been used. | Retrieve the existing operation; do not replace the reference blindly. |
| Completed response | The operation reached the documented terminal state. | Persist the result and prevent another submission of the same intent. |
Submit a stable operation#
The following sequence demonstrates a backend-owned operation reference. The route and fields are illustrative. Replace them with the documented contract for the selected endpoint.
import { randomUUID } from "node:crypto";
// A unique database constraint on intentId prevents parallel creation.
const operation = await operations.findOrCreate({
intentId: checkout.id,
reference: randomUUID(),
amount: checkout.amount,
currency: checkout.currency,
state: "CREATED",
});
const result = await sendTransfer({
externalRefNo: operation.reference,
amount: operation.amount,
currency: operation.currency,
recipientRef: checkout.recipientRef,
});
await operations.recordResult(operation.id, result);Use a durable database record, not component state alone. Disabling a submit button improves the interface but does not prevent two tabs, parallel servers, or a recovered request from making duplicate calls.
Retry with the same meaning#
Keep the original reference attached to the original recipient, amount, currency, and business intent. If those values change, create a new intentional operation only after resolving or cancelling the original through its documented workflow.
A backend may reject the same reference with a changed payload, return an existing result, or retain a result for a specified period. These semantics must be confirmed for each endpoint. Do not assume that idempotency is permanent or shared across tenants and environments.
- Read the persisted operation and its last known state.
- Reconcile uncertain outcomes with the documented lookup or requery request.
- Retry only when the endpoint contract says that retry is safe.
- Use bounded attempts, backoff, and jitter instead of an unbounded loop.
- Keep the reference stable while generating fresh authentication timestamps and signatures.
Bill-payment recovery in KlemPay#
The existing KlemPay bill-payment flow uses a persisted externalRefNo. When the backend returns PAY-BIL-012, the interface explains that the reference was already used and attempts to retrieve an existing receipt if a payment reference is available.
For PAY-BIL-006, the same flow refreshes customer validation and retries with the original external reference. Product-controlled amounts come from the refreshed backend validation. This is a specific implemented recovery path, not permission to retry every error or override a backend amount.
Reconcile asynchronous updates#
Both status requests and webhooks can report the same operation. Apply them through a shared state transition function that validates the resource reference and refuses invalid regressions. A late pending event must not overwrite a confirmed final result.
Use an event identifier to deduplicate delivery when one is supplied, and use the financial operation reference to connect an event to the correct business record. See Webhooks for durable acknowledgement and replay handling.
Test duplicate and delayed requests#
- Submit the same reference twice sequentially and concurrently.
- Retry after a simulated network timeout while the server completes the first request.
- Send the same reference with a changed amount and confirm the documented rejection.
- Deliver a webhook twice and confirm only one ledger effect is applied.
- Deliver events out of order and verify final states do not regress.
- Restart the application between submission and confirmation to prove the reference survives.