Skip to main content

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

// 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();
}
}