Preventing duplicate writes during Ballerina client retries
0 reputation · 23 Jan 2023, 09:00 UTC
0 reputation · 23 Jan 2023, 09:00 UTC
Ballerina provides built-in retry mechanisms for service clients to manage transient failures. However, when performing non-idempotent operations—such as POST requests to a database or external API—automatic retries can lead to duplicate resource creation if the initial request was processed but the response was lost.
While custom headers can be used to pass idempotency keys to downstream services, the language does not currently provide a native primitive to automatically generate and track a unique token across all attempts of a single retry loop.
What is the recommended pattern for managing idempotency keys within a Ballerina retry block to ensure a consistent identifier is used across all attempts without manually wrapping every client call in a custom state-tracking object?
33275 reputation · 23 Jan 2023, 19:12 UTC
Generate the idempotency key once, before entering the retry block, store it in a variable whose scope covers the whole retry, and attach it as a header on every attempt inside the block. Ballerina's retry mechanism re-executes the block body, but it does not reset variables declared outside it—so a key created outside the loop is naturally stable across attempts. No state-tracking wrapper object is needed; lexical scoping does the work for you.
import ballerina/http;
import ballerina/uuid;
import ballerina/lang.value;
function createPayment(http:Client api, PaymentRequest req) returns error? {
// 1. Generate the key ONCE, outside the retry scope.
string idempotencyKey = uuid:createType4AsString();
// Alternative: derive it deterministically from the logical request,
// e.g. a hash of (accountId, amount, clientReference), so a caller-side
// full retry of the whole operation also deduplicates.
retry (3) {
http:Response res = check api->post("/payments", req,
{"Idempotency-Key": idempotencyKey});
if res.statusCode >= 500 {
// Force another attempt on transient server errors.
fail error value:fromJsonStringWithType("transient failure");
}
// 2xx (including a replayed success from the server) ends the loop.
}
}
Two properties make this safe:
Idempotency-Key header.The likely explanation for duplicate writes in your setup is that a key (if any) is being generated inside the retry block, producing a different value per attempt, or that no key is sent at all. Moving generation outside the block fixes the first case. Confirmed facts are narrower: Ballerina retries re-run the block body, outer-scope bindings persist, and http:Client lets you set per-request headers. The language has no built-in idempotency primitive, so the header contract is entirely yours.
Important limits:
Idempotency-Key, you must deduplicate another way (unique constraints, deterministic resource IDs) or avoid retrying non-idempotent calls.If your downstream API documents a different deduplication header (some use X-Request-Id or a body field), substitute that name—the pattern is unchanged.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.