← All articles
Backend1 min read

Building Idempotent REST Endpoints

Clients retry. Networks drop responses. An idempotency key lets a repeated request produce the same result exactly once.

S

Swapnika Voora

Author

A client that never gets your response will retry — and without protection, a retried "create payment" can charge twice. Idempotency keys let a client safely repeat a request, guaranteeing the operation happens at most once.

Accept an idempotency key

The client generates a unique key per logical operation and sends it as a header. The same key means "this is the same request."

PaymentController.java
@PostMapping("/api/payments")
public ResponseEntity<Receipt> pay(
        @RequestHeader("Idempotency-Key") String key,
        @Valid @RequestBody PaymentRequest request) {
    Receipt receipt = payments.process(key, request);
    return ResponseEntity.ok(receipt);
}

Store the result against the key

On first sight of a key, do the work and persist the result. On a repeat, return the stored result instead of doing the work again.

PaymentService.java
@Transactional
public Receipt process(String key, PaymentRequest request) {
    return idempotencyRepo.findByKey(key)
        .map(record -> deserialize(record.getResult()))
        .orElseGet(() -> {
            Receipt receipt = gateway.charge(request);
            idempotencyRepo.save(new IdempotencyRecord(key, serialize(receipt)));
            return receipt;
        });
}

Guard the race

Two retries can arrive at once. A unique constraint on the key column turns the race into a catchable database error instead of a double charge.

schema.sql
CREATE TABLE idempotency_record (
    key VARCHAR(64) PRIMARY KEY,
    result JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now()
);

Takeaways

  • Require an Idempotency-Key header on unsafe, non-idempotent operations.
  • Persist the result so repeats return the original response.
  • A unique constraint closes the concurrent-retry race safely.

Expire old keys on a schedule — they only need to live as long as a client might reasonably retry.

#java#spring-boot#rest#reliability

More in Backend