java 149 lines · 3 tabs

Idempotent POST Handling in Spring Boot with a Stored Idempotency-Key

Shared by codesnips Aug 2026
3 tabs
@Entity
@Table(name = "idempotency_records",
       uniqueConstraints = @UniqueConstraint(columnNames = "idempotency_key"))
public class IdempotencyRecord {

    public enum Status { IN_PROGRESS, COMPLETED }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "idempotency_key", nullable = false, updatable = false)
    private String idempotencyKey;

    @Column(name = "request_hash", nullable = false)
    private String requestHash;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Status status;

    @Column(name = "response_status")
    private Integer responseStatus;

    @Column(name = "response_body", columnDefinition = "text")
    private String responseBody;

    @Column(name = "created_at", nullable = false)
    private Instant createdAt = Instant.now();

    protected IdempotencyRecord() {}

    public IdempotencyRecord(String idempotencyKey, String requestHash) {
        this.idempotencyKey = idempotencyKey;
        this.requestHash = requestHash;
        this.status = Status.IN_PROGRESS;
    }

    public boolean isCompleted() {
        return status == Status.COMPLETED;
    }

    public void complete(int responseStatus, String responseBody) {
        this.status = Status.COMPLETED;
        this.responseStatus = responseStatus;
        this.responseBody = responseBody;
    }

    public String getRequestHash() { return requestHash; }
    public Integer getResponseStatus() { return responseStatus; }
    public String getResponseBody() { return responseBody; }
}
3 files · java Explain with highlit

This snippet shows the classic idempotent-POST pattern used by payment and order APIs, where a client supplies an Idempotency-Key header so that retries (from timeouts, network blips, or double-clicks) never create duplicate side effects. The approach hinges on a durable record keyed by that value: the first request does the real work and stores its response, while any replay returns the stored response instead of executing again.

The IdempotencyRecord entity models that durable row. It stores the client-supplied idempotencyKey as a unique column, the requestHash of the payload, an enum status (IN_PROGRESS or COMPLETED), and the serialized responseBody plus responseStatus captured on first success. The unique constraint on idempotency_key is the linchpin: it turns a race between two concurrent replays into a database-level conflict rather than two duplicate charges.

IdempotencyService orchestrates the logic. begin attempts to insert a fresh IN_PROGRESS row and relies on saveAndFlush to surface a DataIntegrityViolationException when the key already exists; that exception is caught and translated into loading the existing record. If the existing record is COMPLETED, a replay is returned; if it is still IN_PROGRESS, the service throws ConcurrentRequestException so the caller can respond 409, avoiding two workers processing the same key at once. The stored requestHash is compared against the incoming payload so that reusing a key with a different body is rejected as a client error rather than silently returning the wrong cached response.

PaymentController ties it together. It requires the Idempotency-Key header, computes a SHA-256 requestHash, and calls idempotency.begin. On a cache hit it short-circuits and returns the stored ResponseEntity; otherwise it performs the charge exactly once and calls complete to persist the outcome for future replays.

A key trade-off is that the IN_PROGRESS marker must be cleaned up or expired if the first attempt crashes mid-flight, otherwise the key stays locked. Teams typically add a TTL sweep or store keys with an expiry. This pattern is worth reaching for whenever a non-idempotent write is exposed over an unreliable network.


Related snips

Share this code

Here's the card — post it anywhere.

Idempotent POST Handling in Spring Boot with a Stored Idempotency-Key — share card
Link copied