Member-only story
Your Go service isn’t idempotent — just because you added an idempotency key
The hidden failure modes that make seemingly safe requests dangerous
So you added an Idempotency-Key header, stored the key in a table, and wrote a test that sends the same request twice. Green. You ship it.
Three weeks later, a customer is charged twice.
An idempotency key is an input. Idempotency is a property of your system under failure. The gap between the two is where money gets duplicated.
Let’s walk through the failure modes, in the order they usually bite.
1. The check-then-act race
Here is the version most of us write first:
func (s *Service) Transfer(ctx context.Context, key string, req TransferRequest) (*TransferResponse, error) {
// 1. Have we seen this key?
if resp, found := s.store.Get(ctx, key); found {
return resp, nil
}
// 2. Do the work
resp, err := s.doTransfer(ctx, req)
if err != nil {
return nil, err
}
// 3. Remember the result
s.store.Put(ctx, key, resp)
return resp, nil
}It passes the “send twice” test because the requests are sequential. Now send them concurrently, which is exactly what a client with an aggressive timeout and a retry loop does:






