Idempotência
Uma operação é idempotente se executá-la múltiplas vezes produz o mesmo resultado que executá-la uma única vez. Em sistemas distribuídos, onde retries e duplicatas são inevitáveis, a idempotência é a garantia que torna essas repetições seguras.
Intenção
Idempotência garante que uma operação pode ser executada qualquer número de vezes com o mesmo efeito final de uma única execução. Isso transforma operações sensíveis — pagamentos, criação de pedidos, envio de mensagens — em operações seguras para retry automático, sem o risco de duplicar efeitos colaterais.
Em matemática, uma função f é idempotente quando f(f(x)) = f(x)
para todo x no seu domínio. Em sistemas de software, o conceito se traduz para
operações: executar a operação uma ou dez vezes deve deixar o sistema no mesmo
estado final. A segunda execução é um no-op — não causa novos efeitos.
Problema
Redes falham. Servidores reiniciam. Timeouts acontecem. Em todos esses casos, o cliente não sabe se a operação foi executada ou não — e a resposta natural é tentar de novo. O problema é que "tentar de novo" em operações com efeito colateral pode causar resultados indesejados:
- Cobrança duplicada: o cliente de pagamento envia uma requisição de cobrança, não recebe resposta (timeout de rede), e retenta. Se o servidor processou a primeira requisição mas a resposta foi perdida na rede, o retry cria uma segunda cobrança. O usuário é cobrado duas vezes por uma única compra.
- Pedido duplicado: um webhook de confirmação de pagamento é entregue duas vezes pelo provedor (comportamento normal em sistemas at-least-once). O sistema de pedidos que processa o webhook cria dois pedidos para a mesma compra.
- E-mail duplicado: uma job de notificação falha na metade e é reexecutada. Os usuários que já receberam o e-mail antes da falha recebem novamente.
- Inconsistência de estado: um comando de atualização de saldo executado duas vezes debita o valor duas vezes, mesmo que o usuário tenha executado a operação uma única vez.
O problema fundamental: o sistema não consegue distinguir uma nova intenção do usuário de uma repetição causada por falha de rede. Sem idempotência, retry é uma fonte de bugs silenciosos.
Como funciona
Operações naturalmente idempotentes
Algumas operações são idempotentes por definição — não é necessário nenhum mecanismo adicional:
- GET: buscar um recurso não muda o estado do servidor. Executar cem vezes retorna o mesmo resultado (supondo que o estado não mudou por outras operações).
-
PUT (sobrescrição total): definir o estado de um recurso
para um valor específico.
PUT /users/42 {"name": "João"}duas vezes resulta no mesmo estado que uma única execução. - DELETE: remover um recurso. O segundo DELETE de um recurso inexistente pode retornar 404 em vez de 200, mas o estado do servidor é o mesmo: o recurso não existe.
Operações não idempotentes por natureza incluem POST (cada
chamada pode criar um novo recurso), incremento de contador
(UPDATE saldo = saldo + 100) e qualquer operação cujo efeito
depende do número de execuções.
Idempotency Key — tornando operações não-idempotentes seguras
A técnica padrão para adicionar idempotência a operações não-idempotentes é a Idempotency Key: um identificador único gerado pelo cliente para cada tentativa de operação (não para cada retry — para cada operação distinta do usuário).
Cliente gera: idempotency-key = uuid-v4() por operação do usuário
(o mesmo UUID é reutilizado em todos os retries dessa operação)
Primeira execução:
┌──────────┐ POST /pagamentos ┌──────────────┐
│ Cliente │ Idempotency-Key: a1b2-c3d4 ──────► │ Servidor │
└──────────┘ └──────┬───────┘
│ Chave nova?
│ SIM
▼
Processa pagamento
Salva resultado com chave a1b2-c3d4
Retorna: { status: "aprovado", id: 99 }
Retry (timeout na primeira tentativa):
┌──────────┐ POST /pagamentos ┌──────────────┐
│ Cliente │ Idempotency-Key: a1b2-c3d4 ──────► │ Servidor │
└──────────┘ └──────┬───────┘
│ Chave nova?
│ NÃO — já processada
▼
Retorna resultado cacheado:
{ status: "aprovado", id: 99 }
(sem reprocessar o pagamento)
O servidor armazena o resultado da primeira execução associado à chave. Retries com a mesma chave retornam o resultado cacheado sem reprocessar a operação. Do ponto de vista do cliente, ambas as respostas são idênticas — ele não precisa saber se foi a primeira execução ou um retry.
At-least-once + idempotência = segurança
Sistemas de filas de mensagem com semântica at-least-once garantem que a mensagem será entregue pelo menos uma vez — mas podem entregar mais de uma vez em caso de falha do consumer ou rebalanceamento. Um consumer idempotente processa duplicatas sem consequências:
- A mensagem contém um ID único de evento.
- O consumer registra os IDs já processados.
- Ao receber a mensagem, verifica se o ID já foi processado: se sim, descarta; se não, processa e registra.
Onde aplicar
- Endpoints de pagamento: a operação de maior risco — cobrar duas vezes tem consequência direta para o usuário e para o negócio.
- Criação de pedido/recurso: qualquer endpoint que cria uma entidade com efeito no mundo real.
- Envio de notificações: e-mail, SMS, push notification — o usuário não deve receber a mesma mensagem múltiplas vezes.
- Webhooks recebidos: provedores externos podem retentar webhooks — o receptor precisa ser idempotente.
- Consumers de fila com at-least-once: duplicatas são entregues; o consumer deve tratá-las.
Quando usar
- Qualquer operação exposta a retry automático: timeout de rede, falha de instância, retry configurado no cliente HTTP. Se o cliente pode tentar mais de uma vez, o servidor deve ser idempotente.
- Consumers de filas com at-least-once: Kafka, RabbitMQ, SQS standard — a garantia de entrega implica possibilidade de duplicata. A responsabilidade de lidar com duplicatas é do consumer.
- Webhooks recebidos de sistemas externos: Stripe, GitHub, Shopify e a maioria dos provedores de webhook documentam explicitamente que podem retentar. O receptor deve ser idempotente por design.
- Operações com efeito colateral externo irreversível: cobranças, débitos, reservas de estoque, envios. O custo de duplicar esses efeitos é alto demais para depender apenas de uma única entrega bem-sucedida.
Quando evitar (ou adaptar)
- Quando múltiplos efeitos são a intenção: se o usuário clica "adicionar ao carrinho" duas vezes, provavelmente quer dois itens — não um. Nesse caso, a Idempotency Key deve ser gerada explicitamente pelo usuário (ou pela ação), não automaticamente pelo cliente. O design deve deixar claro o que é repetição acidental e o que é intenção.
- Operações de leitura: GET já é idempotente por natureza. Adicionar mecanismo de Idempotency Key a endpoints de leitura é overhead sem benefício.
Prós e contras
Prós
- Retry seguro: o cliente pode tentar novamente sem medo de duplicar efeitos, simplificando a lógica de tratamento de erros.
- Resiliência natural: falhas de rede, timeouts e reinicializações de servidor se tornam eventos recuperáveis, não causas de inconsistência.
- Compatibilidade com at-least-once: sistemas de fila e webhooks podem ser usados com segurança sem exigir semântica exactly-once (mais cara e complexa).
- Auditabilidade: armazenar a Idempotency Key com o resultado cria um registro das operações tentadas, útil para debugging e auditoria.
Contras
- Armazenamento da chave: o servidor precisa persistir as chaves processadas e os resultados associados, adicionando storage e complexidade operacional.
- Definição do escopo da chave: determinar o que constitui "a mesma operação" vs "uma nova operação" pode ser sutil e levar a bugs se errado.
- Efeitos colaterais externos difíceis de controlar: a operação principal pode ser idempotente, mas chamar serviços externos (e-mail, SMS) também precisa ser, o que depende de capacidade de terceiros.
- Expiração da chave: decidir por quanto tempo manter as chaves requer análise do padrão de retry esperado — muito curto perde proteção, muito longo aumenta storage.
Armadilhas comuns
1. Idempotency Key com escopo errado
Usar o ID do usuário, o ID do produto ou qualquer dado da operação como chave de idempotência em vez de um UUID gerado por tentativa é o erro mais comum. Se a chave é o ID do usuário, todas as tentativas do mesmo usuário para operações diferentes são tratadas como a mesma operação — o segundo pagamento do mês retorna o resultado cacheado do primeiro.
A Idempotency Key deve identificar uma tentativa específica de uma operação específica. A forma correta é: o cliente gera um UUID novo para cada operação distinta que o usuário inicia, e reutiliza esse mesmo UUID em todos os retries daquela operação.
2. Expirar a chave cedo demais
Se o cache da Idempotency Key expira em 5 minutos mas o retry pode vir em 1 hora — por exemplo, via dead letter queue reprocessada manualmente — a proteção desaparece exatamente quando ela seria necessária. A janela de retenção da chave deve cobrir o período máximo possível entre a primeira tentativa e o último retry, incluindo cenários de processamento tardio.
3. Idempotência parcial — efeitos colaterais duplicados
A operação principal (criar um pedido) é idempotente com Idempotency Key, mas os efeitos colaterais (envio de e-mail de confirmação, disparo de webhook para o ERP, criação de evento no sistema de analytics) não são. O resultado: um único pedido criado, mas três e-mails de confirmação, três webhooks disparados e três eventos no analytics.
Regra prática: ao tornar uma operação idempotente, mapeie todos os efeitos colaterais que ela gera e avalie a idempotência de cada um. Efeitos colaterais externos que não podem ser tornados idempotentes devem ser movidos para fora do fluxo de retry ou protegidos com verificação de estado antes da execução.
4. Confundir idempotência com segurança a concorrência
Idempotência protege contra retries sequenciais — a mesma operação executada uma vez e depois de novo. Não protege contra concorrência: dois requests simultâneos com chaves diferentes (dois usuários clicando ao mesmo tempo) ou dois processos processando a mesma mensagem de fila ao mesmo tempo. Para esses cenários, é necessário lock distribuído ou transação atômica com verificação de unicidade — idempotência resolve um problema diferente.
Arquiteturas e padrões relacionados
Circuit Breaker e idempotência são complementares na estratégia de resiliência: o Circuit Breaker decide se vale tentar a chamada, e a idempotência garante que, quando a chamada é retentada após uma falha, o efeito não é duplicado. Juntos, permitem retry com segurança: o Circuit Breaker evita retry quando o serviço está degradado; a idempotência protege quando o retry é necessário.
No CQRS, commands idempotentes são mais seguros para operar em sistemas distribuídos: um command de "processar pagamento" que pode ser reenviado sem efeito duplicado simplifica o tratamento de falhas no fluxo de escrita. O Event Store também se beneficia: eventos gerados por commands idempotentes não se duplicam mesmo que o command seja processado mais de uma vez.
Filas de Mensagem com semântica at-least-once são o contexto mais comum onde a idempotência é obrigatória. A combinação fila + consumer idempotente é o padrão de fato para processamento confiável de mensagens sem exigir a complexidade e o custo de semântica exactly-once.