PrincípioIntermediário7 min de leitura6 seções1999, Data aproximada

Idempotência

Executar a mesma operação duas vezes tem o mesmo efeito de executar uma: é o que torna o retry seguro. Em rede, onde 'não sei se foi' é resposta comum, é a diferença entre reenviar e cobrar em dobro.

Em 10 segundos

Fazer duas vezes = fazer uma. O cliente manda a mesma chave em todo retry; o servidor executa só na primeira e, nas repetições, devolve o resultado guardado. Sem isso, todo timeout vira uma roleta de cobrança dupla.

Antes de ter nome

Idempotente vem da álgebra de Peirce, em 1870 — 'a operação aplicada de novo dá o mesmo resultado' —, muito antes de virar regra de retry em sistemas distribuídos.

  • #retry
  • #idempotency-key
  • #pagamentos
  • #http
  • #distribuidos
Analogia

O botão do elevador

Apertar o botão cinco vezes não chama cinco elevadores: a primeira aperta, as outras encontram o pedido já registrado e não mudam nada. O painel 'lembra' que aquela intenção já existe. A chave de idempotência é isso — um jeito de o servidor reconhecer 'esse pedido eu já vi' e não executar de novo.

01Conceito

O problema: o timeout não diz o que aconteceu

Seu app chama a API de pagamento e a conexão cai depois de 30 segundos. O dinheiro saiu? Você não sabe: o timeout pode ter acontecido antes de o servidor processar ou depois — a resposta é que se perdeu, não necessariamente a operação.

A partir daí só existem duas escolhas ruins: reenviar (e arriscar cobrar duas vezes) ou desistir (e arriscar não cobrar). Retry automático — que toda biblioteca de HTTP e toda fila fazem por padrão — transforma essa raridade em rotina.

O nome vem da matemática: uma operação é idempotente quando f(f(x)) = f(x). Em HTTP, GET, PUT e DELETE são idempotentes por contrato — 'apagar o recurso 42' duas vezes deixa o mundo igual. POST não é: 'criar um pagamento' duas vezes cria dois. É por isso que a chave de idempotência vive quase sempre em POSTs.

Entrega ao-menos-uma-vez não é defeito das filas — é o custo de não perder mensagem. Kafka, RabbitMQ e SQS reentregam na dúvida. A pergunta nunca é 'como evitar duplicatas na entrega' (não dá), e sim 'como tornar o processamento imune a elas'.

Como se encaixa
  1. Cliente
  2. POST + Idempotency-KeyAPI
  3. chave inédita?Registro de chaves
  4. só se for a primeira vezProcessamento
A chave é checada antes de qualquer efeito: repetida, a API devolve o resultado gravado e o processamento nem fica sabendo do retry.
02Anatomia

As três peças

Camada 1/3Cliente

Camada 1

Cliente

gera a chave — uma por operação, não por request

A chave nasce junto com a intenção ('pagar o pedido 8231') e se repete em todo retry daquela intenção. Pode ser um UUID guardado com o pedido ou um hash determinístico da operação. O erro clássico é gerar no momento do envio: cada retry ganha chave nova e a proteção evapora.

Se violar: chave nova a cada tentativa = servidor vê operações 'diferentes' e executa todas. A idempotência morre no cliente, silenciosamente.

03Implementação

Código

// A chave de idempotência transforma "cobrar" em "garantir que foi cobrado".
interface Resultado {
  status: number;
  corpo: unknown;
}

// chave -> resultado da primeira execução (Redis/tabela com UNIQUE na vida real)
const executadas = new Map<string, Resultado>();

async function criarPagamento(chave: string, valor: number): Promise<Resultado> {
  // 1. Já vi essa chave? Devolve O MESMO resultado, sem executar de novo.
  const anterior = executadas.get(chave);
  if (anterior) return anterior;

  // 2. Inédita: executa e guarda o resultado ANTES de responder.
  //    Em produção, o "guardar" precisa ser atômico (INSERT com UNIQUE):
  //    duas chamadas simultâneas com a mesma chave não podem passar juntas.
  const cobranca = { id: crypto.randomUUID(), valor, status: "paga" };
  const resultado: Resultado = { status: 201, corpo: cobranca };
  executadas.set(chave, resultado);
  return resultado;
}

// O cliente gera UMA chave por operação e a repete em todo retry:
const chave = "pedido-8231-pagamento"; // derivada da operação, não do request
await criarPagamento(chave, 100); // executa e cobra
await criarPagamento(chave, 100); // timeout? retry? -> mesmo resultado, R$100 uma vez
04No mundo real

Casos de uso

Cobrança PIX com retry do integrador

Cenário

Um PaaS de pagamentos expõe POST /pix. O integrador tem retry automático com backoff, e conexões caem todo dia — timeout depois que o PIX já saiu significa segunda chamada em segundos.

Como o padrão entra

Todo POST exige Idempotency-Key. A chave é gravada com UNIQUE junto do resultado; o retry bate na constraint e recebe a mesma resposta 201 com o mesmo id de transação. O integrador pode reenviar sem medo — a API garante no máximo um PIX por chave.

Trade-off: Uma tabela a mais, com escrita no caminho crítico de toda cobrança, e a política de TTL vira contrato público: mudou o prazo, muda o comportamento do retry de todo cliente.

Consumidor de fila que reprocessa

Cenário

O worker consome 'pedido-pago' e dá baixa no estoque. A fila entrega ao-menos-uma-vez: rebalanceamento, crash antes do ack ou timeout de visibilidade fazem a mesma mensagem chegar duas vezes.

Como o padrão entra

O id do evento é a chave: a baixa registra 'evento X processado' na mesma transação que decrementa o estoque. Redelivery encontra o registro e vira no-op. O efeito acontece exatamente uma vez, mesmo com a entrega acontecendo três.

Trade-off: O registro de eventos processados cresce com o tráfego e precisa de expurgo; e a deduplicação só vale dentro da janela retida — reprocessamento de dias depois exige outra estratégia.

05Cuidado

Armadilhas

  1. Chave por request em vez de por operação

    Se a chave é gerada na hora do envio, cada retry cria uma chave nova — e o servidor, corretamente, executa todas. A chave pertence à intenção de negócio: nasce com o pedido e sobrevive a quantas tentativas forem necessárias.

  2. Checar e gravar em dois passos

    SELECT para ver se a chave existe e INSERT depois é uma race condition clássica: duas chamadas simultâneas passam pela checagem antes de qualquer INSERT e executam as duas. A trava tem que ser uma operação atômica — UNIQUE no banco ou SET NX no Redis.

  3. Lembrar a chave mas esquecer o resultado

    Responder 'já processado' sem corpo deixa o cliente que perdeu a primeira resposta no escuro: ele precisava do id da cobrança. Idempotência de verdade devolve a mesma resposta, não um aviso de que a resposta existiu.

  4. Confiar que 'só acontece uma vez'

    Retry não é cenário raro: é o comportamento padrão de axios, fetch com retry, service meshes e toda fila. Desenhar o happy path e tratar duplicata como exceção inverte a realidade — a duplicata é parte do contrato de rede.

06Decisão

Quando usar × quando evitar

Use quando

3
  • Operações com efeito externo (dinheiro, e-mail, estoque) atrás de qualquer retry.
  • Consumidores de mensageria com entrega ao-menos-uma-vez.
  • APIs públicas — você não controla o comportamento de retry dos clientes.

Evite quando

3
  • Operações já idempotentes por natureza (PUT de estado completo, DELETE por id).
  • Leituras puras — repetir um GET não tem efeito colateral.
  • Fluxos internos onde a operação inteira já roda numa única transação com constraint natural (ex.: UNIQUE no par pedido+tipo).

Testar o que ficou

4 armadilhas de Idempotência viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • Idempotency-Key do StripeRepetir a mesma cobrança com a mesma chave devolve o primeiro resultado em vez de cobrar de novo — a rede pode reenviar sem medo.
  • PUT e DELETE do HTTPPor especificação, aplicar o mesmo PUT dez vezes deixa o recurso no estado que uma deixaria; é o que torna o retry seguro nesses verbos.
  • kubectl applyAplicar o mesmo manifesto repetidas vezes reconcilia para o estado desejado sem criar cópias — o comando descreve o fim, não o passo.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Mesma chave → mesmo efeito, mesmo se repetir.
await cobrar({ idempotencyKey: key, valor });

O que ele cobra

Nenhum salto a mais entre a chamada e o efeito.

  • Guardar a chave de idempotência e o resultado de cada operação já processada custa armazenamento e limpeza
  • Definir o que conta como 'a mesma' operação é mais sutil do que parece

Não vale se: a operação já é naturalmente idempotente, como um PUT que sobrescreve — aí adicionar controle de chave é redundante.

Onde isto já custou caro

Incidentes reais que este conceito explica.

Onde isto aparece

Continue por aqui