Retry without idempotency
It was retried, and it worked twice.
What it looks like
- A retry wrapper around an operation that creates or charges something.
- A retry on a timeout, where the first attempt may have succeeded after the client gave up.
- An idempotency key generated inside the retry loop rather than outside it.
Why it survives review
The retry is obviously correct — the code retries, the flake goes away, the metrics improve. The double effect only happens when the first attempt actually succeeded and the *response* was lost, which is rare and looks like a different bug when it happens.
How to see it
- For every retried operation, ask whether running it twice is the same as running it once. If not, the retry needs a key.
- A timeout is not a failure. It means you do not know, and 'do not know' must not be retried like 'did not happen'.
- Check the idempotency key is generated once per logical operation, not once per attempt.
A minimal pair
Correct
const key = newIdempotencyKey();
return withRetry(() => charge(amount, key));
Defective
return withRetry(() => charge(amount, newIdempotencyKey()));
A charge that times out and is retried is now two charges, each with its own key.
Practise it
No exercise in the corpus sets this class yet. The lesson stands on its own — the corpus grows by adding subjects, and pretending otherwise would hide the gap.