Avançado13 min de leituraNível 3

Idempotência

Fazer a mesma coisa duas vezes deveria ter o mesmo efeito de fazer uma vez só.

Introdução

Uma operação é idempotente quando executá-la várias vezes tem o mesmo efeito observável de executá-la uma única vez. Em sistemas distribuídos, onde retries e entregas duplicadas são inevitáveis, é a idempotência que impede que uma nova tentativa vire uma segunda cobrança, um pedido duplicado ou um saldo dobrado.

Por que existe

A rede não é confiável. O cliente envia uma requisição, o servidor processa, mas a resposta se perde no caminho — então o cliente tenta de novo. Brokers de mensagens entregam pelo menos uma vez (at-least-once), então consumidores veem duplicatas. Sem idempotência, esses eventos normais e esperados causam dano real: cobrar duas vezes, enviar o produto duas vezes, disparar o mesmo e-mail duas vezes. A idempotência existe para tornar o retry seguro — e é isso que torna sistemas confiáveis possíveis.

Analogia

O botão de um elevador bem projetado é idempotente: apertar 'chamar' cinco vezes traz um elevador, não cinco. Apertar de novo enquanto ele já está vindo não muda nada. Compare com um botão ingênuo de 'somar R$ 10' apertado cinco vezes — você ficaria devendo R$ 50. Operações bem desenhadas se comportam como o botão do elevador.

Como funciona

Algumas operações já são naturalmente idempotentes: SET balance = 100 (valor absoluto) ou DELETE user 42 levam ao mesmo estado não importa quantas vezes você aplique. Outras não são: balance = balance + 10 (valor relativo) ou INSERT order produzem um efeito novo a cada execução.

Para tornar uma operação não idempotente segura, o cliente envia uma idempotency key — um token único por operação lógica (por exemplo, um UUID por tentativa de checkout). O servidor guarda essa chave junto com o resultado da primeira execução. Em um retry com a mesma chave, ele devolve o resultado armazenado em vez de executar de novo. A chave precisa ser gravada de forma atômica com o efeito (mesma transação), senão você reabre a janela de duplicação. A semântica do HTTP reflete isso: GET/PUT/DELETE são definidos como idempotentes; POST não é — por isso POSTs de pagamento carregam uma idempotency key.

  1. 1

    O cliente gera uma idempotency key única para a operação e a envia junto com a requisição.

  2. 2

    O servidor consulta o store: essa chave já foi vista antes?

  3. 3

    Se for nova, executa a operação e grava (key → resultado) de forma atômica com o efeito.

  4. 4

    Se já foi vista, devolve o resultado gravado anteriormente, sem executar de novo.

  5. 5

    O cliente pode fazer retry quantas vezes precisar; o resultado é idêntico todas as vezes.

Interativo

Arquitetura

A idempotency key deduplica uma requisição repetida

Clienteenvia key: abc-123

Verifica a chave

Serviço de Pagamento

Store de dedupe (atômico com o efeito)

Store de idempotênciaabc-123 → resultado
DB de Pagamentos

Primeira chamada: executa + grava (key, resultado) de forma atômica. Retry com a mesma key: devolve o resultado guardado, sem cobrar de novo.

No código

TypeScriptEndpoint de pagamento idempotente (chave gravada na mesma transação)
export async function charge(req: ChargeRequest, idempotencyKey: string) {
  return db.transaction(async (tx) => {
    // Reivindicação atômica: unique constraint em idempotency_key.
    const existing = await tx.idempotency.findByKey(idempotencyKey)
    if (existing) return existing.result        // retry → devolve o resultado guardado

    const result = await processPayment(tx, req) // o efeito colateral de verdade

    // Gravado na MESMA transação do efeito — sem janela de duplicação.
    await tx.idempotency.insert({ key: idempotencyKey, result })
    return result
  })
}
C#Consumer idempotente para entrega at-least-once
public async Task Handle(OrderPlaced evt)
{
    // Dedupe pelo id da mensagem/evento; INSERT ... ON CONFLICT DO NOTHING.
    var isNew = await _processed.TryMarkAsync(evt.EventId);
    if (!isNew) return;                 // entrega duplicada → no-op

    await _orders.CreateAsync(evt);     // seguro: roda no máximo uma vez por EventId
}

No mundo real

Onde você já viu isso

A API da Stripe exige o header Idempotency-Key em requisições de cobrança exatamente por isso: um cliente cuja conexão caiu pode refazer a cobrança com segurança, e a Stripe devolve o resultado original em vez de cobrar duas vezes. É o mesmo padrão que sustenta o processamento 'exactly-once' em sistemas de eventos — que, no fundo, é entrega at-least-once somada a consumers idempotentes.

Quando usar

Recorra a isso quando

  • Qualquer operação com efeito colateral acessível por uma rede não confiável (pagamentos, pedidos).
  • Consumers de brokers at-least-once (Kafka, SQS, RabbitMQ).
  • APIs públicas em que o cliente vai fazer retry ao dar timeout.
  • Jobs em background e webhooks que podem ser reprocessados.

Evite quando

  • Operações naturalmente idempotentes (SET absoluto, DELETE) que não precisam de mecanismo extra.
  • Leituras puras, sem efeito colateral.
  • Quando o store de dedupe realmente não compensa para uma ação de baixo risco e fácil de corrigir.

Trade-offs

Vantagens

  • Torna retries e entregas duplicadas seguros — a base da confiabilidade.
  • Permite semântica 'exactly-once' em cima de uma infraestrutura at-least-once.
  • Simplifica o cliente: ele pode refazer a chamada às cegas em caso de falha.

Desvantagens

  • Exige guardar e expirar chaves (estado extra).
  • A chave precisa ser gerada e escopada corretamente pelo cliente.
  • A atomicidade entre chave e efeito é fácil de errar de forma sutil.
  • Chaves têm janela de retenção; retries muito atrasados podem reexecutar.

A idempotência troca um pouco de estado e complexidade extra pela capacidade de fazer retry com segurança — quase sempre vale a pena para operações com efeito colateral. O desenho da chave é, em si, um trade-off: chaves geradas pelo cliente são corretas, mas exigem cooperação do cliente; chaves naturais (id do pedido) evitam armazenamento, mas só funcionam quando existe um identificador de negócio estável. A retenção também é um trade-off: guarde as chaves tempo suficiente para cobrir retries realistas, mas não para sempre. E 'exactly-once' é, na real, 'entrega at-least-once + processamento idempotente' — fingir o contrário leva a designs frágeis.

Garantias de entrega/processamentoAt-most-onceAt-least-onceExactly-once (efetivo)
DuplicatasNuncaPossíveisRemovidas por dedupe
Mensagens perdidasPossíveisNuncaNunca
ComoDispara e esqueceRetry até o ackAt-least-once + consumer idempotente
Use quandoMétricas que você pode perderMaioria dos eventos de negócioPagamentos, pedidos, dinheiro

Erros comuns

Fique atento a

  • Gravar a idempotency key em um passo separado do efeito, reabrindo a janela de duplicação.
  • Gerar a chave no servidor a cada requisição (fazendo o retry parecer uma chamada nova).
  • Reaproveitar uma mesma chave para operações logicamente diferentes.
  • Não ter unique constraint, permitindo que duas primeiras tentativas concorrentes executem as duas.
  • Confundir entrega at-least-once com exactly-once — o consumer ainda precisa deduplicar.

Pensando em falhas

O que quebra

Uma requisição dá timeout, mas na verdade foi concluída com sucesso. O que acontece com e sem idempotência?

O cliente refaz a chamada. Sem idempotência, o servidor executa de novo — cobrança dobrada. Com uma idempotency key, o servidor reconhece a chave, pula a execução e devolve o resultado original. É exatamente esse cenário (resposta perdida, e não requisição perdida) que faz a idempotência importar mais do que ajustar timeouts.

Um consumer processa a mesma mensagem duas vezes (entrega at-least-once). Como manter a correção?

O consumer precisa ser idempotente: deduplicar pelo id da mensagem/evento antes de aplicar o efeito, de preferência gravando o id de forma atômica com a escrita. Aí uma reentrega vira no-op. Tentar alcançar exactly-once só no broker é uma armadilha — a resposta prática é at-least-once + consumer idempotente.

Pense como sênior

Visão de Engenheiro(a) Sênior

A frase clássica de quem é sênior: 'fazer retries sem garantir idempotência pode executar a mesma operação mais de uma vez e gerar efeitos duplicados.' Se você propõe retry em qualquer ponto, a frase seguinte já tem que ser como aquele alvo foi tornado idempotente.

Visão de Engenheiro(a) Sênior

Empurre a idempotência para a camada que é dona do efeito colateral e grave a chave na mesma transação do efeito. Pendurar o dedupe em um cache separado deixa uma janela de crash que reintroduz duplicatas.

Lembre-se

Fazer retries sem idempotência pode duplicar efeitos colaterais.

Lembre-se

Grave a idempotency key de forma atômica com o efeito, senão o dedupe é uma mentira.

Questões de entrevista

1

Projete um endpoint de pagamento idempotente.

Recordação ativa

Teste-se

Idempotência · Questão 1 / 2Médio

Your payment API receives the same request twice because the client timed out and retried. What is the best protection against a double charge?

Desafios práticos

Médio1–3 horas

Endpoint de pagamento com Idempotency-Key

Construir um endpoint de pagamento que seja seguro sob retries duplicados e concorrentes.

Duas requisições idênticas (mesma Idempotency-Key) precisam resultar em exatamente uma cobrança, mesmo que cheguem no mesmo instante.

Requisitos

  • Aceitar um header Idempotency-Key.
  • Executar a cobrança e persistir key+resultado em uma única transação.
  • Devolver o resultado original em qualquer retry.

Restrições

  • Uma unique constraint na chave.
  • Precisa lidar com duas primeiras tentativas concorrentes.

Critérios de aceite

  • Retries sequenciais → uma cobrança, mesma resposta.
  • Duplicatas concorrentes → uma cobrança, e ambos os chamadores recebem o mesmo resultado.

Casos extremos

  • Crash entre a cobrança e o insert da chave (deve ser impossível se for atômico).
  • Chave reutilizada para um valor diferente.

Bônus

  • Adicionar retenção/expiração de 24h para as chaves.
  • Devolver uma resposta clara para uma duplicata em andamento.

Reflexão

  • Por que a chave precisa ser gravada na mesma transação da cobrança?

Desafio de arquitetura

Um checkout chama o Payment, que emite um evento PaymentSucceeded consumido pelos serviços de Order e Email através de um broker at-least-once. Projete a idempotência ponta a ponta para que um retry do cliente, uma reentrega do Payment e uma reentrega do Order, juntos, nunca consigam cobrar duas vezes, criar dois pedidos ou enviar dois recibos. Onde mora cada dedupe e qual é a chave em cada salto?

Flashcards

Flashcards1 / 4

Resumo

A idempotência torna repetir uma operação inofensivo, o que é essencial porque retries e entregas duplicadas são garantidos em sistemas distribuídos. Operações naturalmente idempotentes não precisam de nada; para as demais, uma idempotency key fornecida pelo cliente e gravada de forma atômica com o efeito deduplica os retries. Processamento 'exactly-once' é simplesmente entrega at-least-once somada a um consumer idempotente.

Pontos-chave

  • Idempotente = fazer N vezes é igual a fazer uma vez (do ponto de vista observável).
  • Use uma idempotency key gravada na mesma transação do efeito colateral.
  • Entrega at-least-once + consumer idempotente = exactly-once efetivo.
  • Nunca adicione retries sem tornar o alvo idempotente.

Suas anotações

Salvo neste dispositivo