title: "Retries and Idempotency: Protecting Business Operations"
date: 2026-08-21
readingTime: 3 min read
tags: ["API", "Architecture", "ERP", "Production"]
A user submits a supplier-payment request. The server accepts it, but the response never reaches the browser. The user clicks again.
The important question is whether that second click represents a new payment or another attempt to complete the original request. A timeout alone cannot answer it.
Retries can help with transient failures. They should be bounded, spaced appropriately, and limited to operations that are safe to repeat. They also need to account for retries already performed by other layers. Microsoft's retry pattern guidance covers these tradeoffs and the importance of idempotency.
For a business write, I would distinguish a definite rejection from an unknown outcome. A validation rejection needs corrected input. An interrupted connection may require checking the operation status before sending another write.
For this hypothetical payment workflow, I would generate an operation key when the user begins submission and reuse it for retries of that submission. Starting a genuinely new payment would create a new key.
The server-side design would associate that key with the authenticated tenant, operation type, a normalized request fingerprint, and the recorded result. A unique database constraint would arbitrate concurrent submissions.
Same key + same request + completed operation -> return recorded result
Same key + same request + pending operation -> return pending status
Same key + different request -> reject conflicting reuse
New key -> begin a new operation
This is an API contract sketch, not a complete payment-processing implementation. Retention matters too: deleting the key too soon could let a delayed retry create another operation.
For an operation contained in one database, I would commit its business change and completion record together. A payment provider introduces a second system. In that case, I would use its supported idempotency mechanism and reconcile uncertain results through its status interface.
A local “completed” flag cannot prove that an external transfer happened. Equally, a local timeout cannot prove that it did not.
Suppose a screen has a ten-second response target. Giving each attempt a ten-second timeout and then adding several retries cannot satisfy that target. I would allocate one overall deadline covering attempts and delays, and stop when that budget is spent.
The interface should then offer a status check for the existing operation. “Still processing” and “failed” should be distinct states with distinct next actions.
Before release, I would test simultaneous duplicate submissions, a response lost after commit, a conflicting payload using the same key, and an external provider that remains unavailable. Each should leave enough recorded information for support to explain what happened.
Retries are useful only when repeating the request preserves the intended business result.