• Payment requests ๐Ÿ’ณ
  • Order creation ๐Ÿ›’
  • Wallet top-ups ๐Ÿ’ฐ
  • OTP / Subscription triggers ๐Ÿ”

If the client retries due to: โœ” Network timeout โœ” Load balancer retry โœ” Kafka redelivery โœ” User double-click โœ” API gateway automatic retry

โ€ฆyou may charge twice or create duplicate orders ๐Ÿ˜ฑ

So we must guarantee:

๐Ÿ” Same request ID = Same result Even if executed multiple times

Let's implement this the correct microservices way ๐Ÿ‘‡

๐Ÿ”‘ What Makes a POST Idempotent?

Client sends a unique Idempotency-Key HTTP header:

Idempotency-Key: cff23e65-bf11-4dfe-a601-a618735b45c2

API checks: โŒ Key exists โ†’ return stored result โœ” Key not found โ†’ process & store result

๐Ÿ—‚๏ธ Step-by-Step Implementation

1๏ธโƒฃ Create an Idempotency Store (Redis recommended)

public interface IdempotencyRepository {
    Optional<String> getResponse(String key);
    void saveResponse(String key, String response);
}

Redis Implementation

@Repository
public class RedisIdempotencyRepository implements IdempotencyRepository {
@Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public Optional<String> getResponse(String key) {
        return Optional.ofNullable(redisTemplate.opsForValue().get(key));
    }
    @Override
    public void saveResponse(String key, String response) {
        redisTemplate.opsForValue().set(key, response, Duration.ofHours(1));
    }
}

โœ” TTL prevents storage explosion โœ” Redis = atomic and super fast

2๏ธโƒฃ Service Layer Wrapper for Idempotency

@Service
public class IdempotencyService {
@Autowired
    private IdempotencyRepository repo;
    public ResponseEntity<?> execute(String key, Supplier<ResponseEntity<?>> action) {
        return repo.getResponse(key)
            .map(cached -> ResponseEntity.ok().body(cached))
            .orElseGet(() -> {
                ResponseEntity<?> result = action.get();
                repo.saveResponse(key, result.getBody().toString());
                return result;
            });
    }
}

3๏ธโƒฃ Controller Example: "Create Payment"

@PostMapping("/payments")
public ResponseEntity<?> createPayment(
        @RequestHeader("Idempotency-Key") String idempotencyKey,
        @RequestBody PaymentRequest req) {
return idempotencyService.execute(idempotencyKey, () -> {
        Payment payment = paymentService.process(req);
        return ResponseEntity.ok(payment);
    });
}

๐Ÿงช Testing the Behavior

Send POST twice with same key:

curl -X POST http://localhost:8080/payments \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 999-111-ABC" \
  -d '{"amount":100,"userId":"U1"}'

๐Ÿ“Œ Result: First call = real processing ๐Ÿ“Œ Second call = cached response returned instantly โœ” Payment executed only once โœจ

๐Ÿ”’ Hardcore Production Concerns

None

Example Redis atomic lock:

boolean locked = redisTemplate.opsForValue()
    .setIfAbsent("lock:"+key, "1", Duration.ofSeconds(10));
if (!locked) {
    throw new RetryableException("Request in progress!");
}

Best Practice Summary Checklist โœ”

None

Idempotency is not optional in event-driven microservices.

It protects users and money It keeps systems correct under retries It prevents duplicate side effects

๐Ÿง  Idempotency = Trust in distributed systems