@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; }
}
@Service
public class IdempotencyService {
private final IdempotencyRecordRepository repository;
public IdempotencyService(IdempotencyRecordRepository repository) {
this.repository = repository;
}
public static final class Outcome {
public final boolean replay;
public final IdempotencyRecord record;
Outcome(boolean replay, IdempotencyRecord record) {
this.replay = replay;
this.record = record;
}
}
@Transactional
public Outcome begin(String key, String requestHash) {
try {
IdempotencyRecord fresh = new IdempotencyRecord(key, requestHash);
repository.saveAndFlush(fresh);
return new Outcome(false, fresh);
} catch (DataIntegrityViolationException conflict) {
IdempotencyRecord existing = repository.findByIdempotencyKey(key)
.orElseThrow(() -> conflict);
if (!existing.getRequestHash().equals(requestHash)) {
throw new IdempotencyKeyReuseException(key);
}
if (!existing.isCompleted()) {
throw new ConcurrentRequestException(key);
}
return new Outcome(true, existing);
}
}
@Transactional
public void complete(IdempotencyRecord record, int status, String body) {
record.complete(status, body);
repository.save(record);
}
}
@RestController
@RequestMapping("/payments")
public class PaymentController {
private final IdempotencyService idempotency;
private final PaymentService payments;
private final ObjectMapper mapper;
public PaymentController(IdempotencyService idempotency,
PaymentService payments,
ObjectMapper mapper) {
this.idempotency = idempotency;
this.payments = payments;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity<String> charge(
@RequestHeader("Idempotency-Key") String key,
@RequestBody ChargeRequest body) throws Exception {
String requestHash = sha256(mapper.writeValueAsBytes(body));
IdempotencyService.Outcome outcome = idempotency.begin(key, requestHash);
if (outcome.replay) {
IdempotencyRecord r = outcome.record;
return ResponseEntity.status(r.getResponseStatus())
.header("Idempotent-Replay", "true")
.body(r.getResponseBody());
}
ChargeResult result = payments.charge(body);
String json = mapper.writeValueAsString(result);
idempotency.complete(outcome.record, 201, json);
return ResponseEntity.status(201).body(json);
}
private static String sha256(byte[] data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(md.digest(data));
}
@ExceptionHandler(ConcurrentRequestException.class)
public ResponseEntity<String> onConcurrent() {
return ResponseEntity.status(409).body("{\"error\":\"request in progress\"}");
}
@ExceptionHandler(IdempotencyKeyReuseException.class)
public ResponseEntity<String> onReuse() {
return ResponseEntity.status(422).body("{\"error\":\"idempotency key reused with different body\"}");
}
}
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.