IdempotencyKey
Key to ensure the same saga does not execute twice.
Concept
The IdempotencyKey prevents duplicate operations from creating multiple sagas. If you fire the same saga with the same key, the second call returns the existing saga.
Usage
When Firing
@PostMapping("/payments")
public ResponseEntity<SagaResponse> createPayment(
@RequestBody PaymentRequest request,
@RequestHeader("Idempotency-Key") String idempotencyKey
) {
PaymentContext context = PaymentContext.from(request);
SagaExecution execution = sagaManager.start(
PaymentProcessingSaga.class,
context,
IdempotencyKey.of(idempotencyKey) // ← Idempotency key
);
return ResponseEntity.accepted().body(
new SagaResponse(execution.sagaId(), execution.idempotent())
);
}
Key Patterns
Request ID (Recommended)
// Client sends a unique UUID
IdempotencyKey.of(request.getRequestId())
Data Composition
// Combines business data
IdempotencyKey.of(
"payment:" + request.getOrderId() + ":" + request.getAmount()
)
Hash
// Hash of the entire payload
String hash = DigestUtils.sha256Hex(objectMapper.writeValueAsString(request));
IdempotencyKey.of(hash)
Behavior
First Call
POST /payments
Idempotency-Key: req-123
→ Saga created with id=saga-001
→ Status: STARTED
Second Call (same key)
POST /payments
Idempotency-Key: req-123
→ Returns existing saga id=saga-001
→ Status: EXECUTING (or COMPLETED if already finished)
Storage
The key is stored in the idempotency_key column of the sagas table:
CREATE TABLE sagas (
...
idempotency_key VARCHAR(255) UNIQUE,
...
);
The UNIQUE index ensures two sagas cannot have the same key.
Considerations
TTL
The key persists as long as the saga exists. For operations that may be repeated after some time, consider including a timestamp in the key:
// Allows retry after 24h
LocalDate today = LocalDate.now();
IdempotencyKey.of("payment:" + orderId + ":" + today)
Collisions
Avoid generic keys that may collide:
// <DocIcon name="x" /> Bad - may collide
IdempotencyKey.of(customerId)
// <DocIcon name="check" /> Good - specific
IdempotencyKey.of(customerId + ":" + orderId + ":" + Instant.now().toEpochMilli())
Full API
public record IdempotencyKey(String value) {
public static IdempotencyKey of(String value) {
return new IdempotencyKey(value);
}
public static IdempotencyKey generate() {
return new IdempotencyKey(UUID.randomUUID().toString());
}
public static IdempotencyKey fromRequest(HttpServletRequest request) {
String header = request.getHeader("Idempotency-Key");
return header != null ? of(header) : generate();
}
}