Post

Idempotência: O Guia Definitivo para Sistemas Distribuídos Resilientes

Idempotência: O Guia Definitivo para Sistemas Distribuídos Resilientes

Imagine que você está em um aplicativo de banco e clica em “Confirmar Pagamento”. A tela trava, a internet oscila e você recebe uma mensagem de erro. Bate o desespero: “Será que pagou?”. Você clica de novo. Sem Idempotência, você acabaria de pagar a mesma conta duas vezes.

Em sistemas distribuídos, falhas de rede não são uma exceção, são uma certeza. A idempotência é a garantia matemática e arquitetural de que, não importa quantas vezes você tente realizar a mesma operação, o efeito colateral no sistema será o mesmo de uma única execução bem-sucedida.

O Conceito Matemático na Computação

Matematicamente, uma função é idempotente se f(x) = f(f(x)). No mundo das APIs, isso significa que múltiplas requisições idênticas devem produzir o mesmo estado final no servidor.

Verbos HTTP e Idempotência Nativa

Nem todo método HTTP nasce igual:

  • GET, HEAD, OPTIONS: São seguros e idempotentes (não alteram estado).
  • PUT: É idempotente. Se você atualiza o nome de um usuário para “Augusto” dez vezes, o nome continuará sendo “Augusto”.
  • DELETE: É idempotente. Deletar algo que já foi deletado resulta no mesmo estado final (o recurso não existe).
  • POST: Não é idempotente. Criar um recurso duas vezes geralmente resulta em dois registros diferentes. É aqui que precisamos da nossa lógica customizada.

A Chave de Idempotência (Idempotency Key)

A maneira mais robusta de implementar idempotência em sistemas distribuídos é através de uma Chave de Idempotência. O cliente gera um identificador único (geralmente um UUID) e o envia no cabeçalho da requisição.

Exemplo de Fluxo no Servidor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@PostMapping("/v1/payments")
public ResponseEntity<PaymentResponse> processPayment(
    @RequestHeader("Idempotency-Key") String idempotencyKey,
    @RequestBody PaymentRequest request) {

    // 1. Verificar se a chave já existe no cache (ex: Redis)
    var cachedResponse = idempotencyService.getResponse(idempotencyKey);
    if (cachedResponse.isPresent()) {
        return ResponseEntity.ok(cachedResponse.get());
    }

    // 2. Tentar adquirir um lock distribuído para esta chave
    try (var lock = distributedLock.acquire(idempotencyKey)) {
        // 3. Processar a lógica de negócio
        var response = paymentService.execute(request);

        // 4. Salvar o resultado no cache com um TTL (ex: 24h)
        idempotencyService.saveResponse(idempotencyKey, response);

        return ResponseEntity.status(201).body(response);
    } catch (LockAcquisitionException e) {
        // 5. Se não conseguiu o lock, significa que há outra requisição em curso
        return ResponseEntity.status(409).build(); // Conflict
    }
}

Funcionamento Interno e Desafios

Race Conditions

Não basta apenas checar se a chave existe. Em sistemas de alta concorrência, duas requisições com a mesma chave podem chegar ao mesmo tempo em instâncias diferentes da sua API. Se ambas checarem o banco e virem que a chave não existe, ambas processarão o pagamento.

A solução: Use um INSERT atômico em um banco de dados ou um SET NX (set if not exists) no Redis. Isso garante que apenas uma thread ganhe o direito de processar aquela chave.

Armazenamento: Banco de Dados ou Redis?

  • Redis: Excelente para performance. Você pode definir um TTL (Time To Live) para que as chaves expirem após 24 ou 48 horas.
  • Banco de Dados (Relacional): Oferece garantias ACID. Você pode salvar a chave de idempotência na mesma transação que altera o saldo do usuário, garantindo integridade absoluta.

Curiosidades Técnicas

  • 409 Conflict vs 200 OK: Alguns puristas defendem que se uma requisição está sendo processada, você deve retornar 409 Conflict. Se ela já terminou, você retorna o resultado original com 200 OK.
  • Determinismo: Para a idempotência funcionar, sua lógica deve ser determinística. Se o resultado de um pagamento depende de um número aleatório gerado no momento do processamento, você terá problemas ao retornar a resposta em cache.

Aplicações Práticas além de APIs

A idempotência também é fundamental em Consumidores de Mensagens (Kafka/RabbitMQ). Como o Kafka garante a entrega at-least-once, seu consumidor pode receber a mesma mensagem duas vezes. A lógica de “Check-and-Insert” usando o ID da mensagem como chave de idempotência é o que evita o processamento duplicado.

Uma reflexão final

Na próxima vez que você estiver desenhando um novo endpoint que altera o estado do sistema, pare um segundo e pergunte-se: “Se o cliente enviar esta requisição e minha resposta se perder no caminho, o que acontecerá quando ele tentar novamente?”. Se a resposta envolver duplicidade de dados ou inconsistência financeira, seu trabalho ainda não terminou. A idempotência é a rede de segurança que permite que sistemas imperfeitos operem de forma perfeita.

This post is licensed under CC BY 4.0 by the author.