Pular para o conteúdo principal

IdempotencyKey

Chave para garantir que a mesma saga não execute duas vezes.


Conceito

A IdempotencyKey evita que operações duplicadas criem múltiplas sagas. Se você disparar a mesma saga com a mesma chave, a segunda chamada retornará a saga existente.


Uso

Ao Disparar

@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) // ← Chave de idempotência
);

return ResponseEntity.accepted().body(
new SagaResponse(execution.sagaId(), execution.idempotent())
);
}

Padrões de Chave

Request ID (Recomendado)

// Cliente envia um UUID único
IdempotencyKey.of(request.getRequestId())

Composição de Dados

// Combina dados do negócio
IdempotencyKey.of(
"payment:" + request.getOrderId() + ":" + request.getAmount()
)

Hash

// Hash de todo o payload
String hash = DigestUtils.sha256Hex(objectMapper.writeValueAsString(request));
IdempotencyKey.of(hash)

Comportamento

Primeira Chamada

POST /payments
Idempotency-Key: req-123

→ Saga criada com id=saga-001
→ Status: STARTED

Segunda Chamada (mesma chave)

POST /payments
Idempotency-Key: req-123

→ Retorna saga existente id=saga-001
→ Status: EXECUTING (ou COMPLETED se já terminou)

Armazenamento

A chave é armazenada na coluna idempotency_key da tabela sagas:

CREATE TABLE sagas (
...
idempotency_key VARCHAR(255) UNIQUE,
...
);

O índice UNIQUE garante que duas sagas não podem ter a mesma chave.


Considerações

TTL

A chave persiste enquanto a saga existir. Para operações que podem ser repetidas após um tempo, considere incluir um timestamp na chave:

// Permite retry após 24h
LocalDate today = LocalDate.now();
IdempotencyKey.of("payment:" + orderId + ":" + today)

Colisões

Evite chaves genéricas que podem colidir:

// <DocIcon name="x" /> Ruim - pode colidir
IdempotencyKey.of(customerId)

// <DocIcon name="check" /> Bom - específico
IdempotencyKey.of(customerId + ":" + orderId + ":" + Instant.now().toEpochMilli())

API Completa

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

Relacionados