Idempotência parece um checkbox. É a diferença entre um retry e uma cobrança duplicada

Este artigo argumenta que idempotência é uma decisão de design ativa, não um checkbox: cobre por que chaves de idempotência não bastam sem uma restrição atômica no banco, sua relação com webhooks, e como testar e monitorar isso corretamente em produção.

Uma pessoa com um marcador, em frente a um quadro branco com anotações, durante uma discussão técnica sobre idempotência.
10 de ago. de 202610 min de leitura
Atualizado em 12 de ago. de 2026

"Só deixa idempotente." Você já disse isso numa revisão de design. Já ouviu isso numa entrevista de system design, quase sempre como a resposta que deveria encerrar a pergunta sobre falhas de rede e retries. É dito tão rápido e com tanta frequência que começa a parecer uma configuração que você simplesmente liga, como habilitar compressão gzip. Não é. Idempotência é uma decisão de design que toca no seu modelo de dados, no seu contrato de API, e em quanto o seu time está disposto a pagar antecipadamente para evitar uma categoria de bug que é brutal de limpar depois que já aconteceu.

A distância entre saber que idempotência importa e realmente implementar isso corretamente é onde muitos engenheiros senior se separam de todo mundo que leu o mesmo post de system design.

O que idempotência promete de fato, e o que não promete

Uma operação idempotente produz o mesmo resultado independente de quantas vezes você a executa com a mesma entrada. Chama uma vez, chama cinco vezes porque a rede engasgou, o estado final é idêntico. Essa é toda a definição, e ela é curta o suficiente para que as pessoas assumam que a implementação também é curta.

Não é, porque idempotência não é uma propriedade de uma única função, é uma propriedade de todo o caminho que uma requisição percorre. Seu endpoint pode estar escrito para ser idempotente e seu sistema ainda assim pode cobrar um cliente duas vezes se a escrita no banco e o passo de "marcar isso como concluído" não fizerem parte da mesma operação atômica. Um retry que chega entre esses dois passos vê um mundo onde a cobrança ainda não foi marcada como completa, tenta de novo, e agora você cobrou a pessoa duas vezes mesmo tendo código "idempotente" nas duas tentativas. A promessa só se sustenta se cada camada abaixo dela também se sustentar.

Chaves de idempotência são o mecanismo, não a solução completa

O padrão comum é uma chave de idempotência gerada pelo cliente e enviada junto com a requisição. O servidor checa se já viu essa chave antes. Se viu, devolve o resultado guardado em vez de repetir o trabalho. Essa é a parte que todo tutorial cobre, e é os vinte por cento fáceis do problema.

Os oitenta por cento difíceis são tudo o que o tutorial pula. O que acontece se duas requisições com a mesma chave chegam ao mesmo tempo, antes de qualquer uma das duas terminar de processar? Sem um lock ou uma restrição de unicidade no nível do banco de dados, você vai processar as duas, anulando todo o sentido da chave. O que acontece se a primeira requisição ainda está em andamento quando a segunda checa e não encontra nenhum resultado guardado ainda? Você precisa de um estado explícito de "em andamento", não só "concluído" e "não visto", ou vai correr uma corrida contra você mesmo. O que acontece seis meses depois, quando alguém reutiliza uma chave de idempotência por acidente, ou uma biblioteca cliente reenvia com uma chave antiga de uma transação já concluída? Você precisa de uma política sobre quanto tempo as chaves vivem e o que acontece quando elas expiram.

Nada disso é exótico. É simplesmente trabalho que não aparece na explicação de um parágrafo, e é exatamente o trabalho que determina se sua implementação de idempotência aguenta tráfego concorrente real ou só as requisições sequenciais que você testou na sua máquina.

O limite da transação do banco de dados é onde a idempotência realmente se sustenta ou cai

Uma verificação de idempotência que não está dentro da mesma transação que a operação que ela protege é decorativa, e essa é a parte incômoda que a maioria dos times ignora. Se você checa se existe uma chave, não encontra nenhuma, faz o trabalho real, e só depois registra a chave, você construiu uma janela onde uma requisição duplicada concorrente pode passar entre a checagem e o registro. Com pouco tráfego você nunca vai ver isso. Com carga real, com retries chegando próximos um do outro justamente porque é quando retries tendem a se agrupar, você vai ver.

A solução é uma restrição de unicidade no nível do banco sobre a chave de idempotência, combinada com uma transação que faz o trabalho e registra a chave de forma atômica, de modo que o próprio banco rejeite a segunda tentativa em vez do seu código de aplicação tentar pegar a corrida depois dos fatos. Isso é menos elegante de escrever e é a diferença entre uma garantia e um melhor esforço. Engenheiros senior recorrem à restrição de banco justamente porque validações no nível da aplicação não conseguem fechar uma condição de corrida que o banco fecha trivialmente.

Idempotência e webhooks são o mesmo problema com outra roupa

Se você já construiu um handler de webhooks, já se deparou com isso. Provedores entregam eventos pelo menos uma vez, o que significa que seu handler precisa ser idempotente contra entregas duplicadas do mesmo ID de evento. O padrão é idêntico ao das chaves de idempotência em pagamentos: guardar o ID do evento, checar antes de processar, usar uma restrição de banco para fechar a corrida, decidir uma janela de retenção. Os domínios parecem diferentes mas o problema de fundo, e a disciplina necessária para resolvê-lo, são os mesmos.

Vale a pena internalizar isso porque significa que idempotência não é uma preocupação exclusiva de pagamentos que você pode deixar num compartimento separado. Em qualquer lugar onde você está do lado que recebe um retry, seja um webhook, uma fila de mensagens com entrega pelo menos uma vez, ou um cliente que reenvia uma requisição depois de um timeout, você está diante do mesmo problema de design com outro rótulo.

Por que "a gente adiciona depois" quase nunca funciona

Idempotência é uma daquelas coisas baratas de construir desde o início e caras de adicionar depois. Adicionar uma chave de idempotência a uma API que já está em produção significa que cada cliente existente precisa começar a enviá-la, o que geralmente exige suportar o comportamento antigo e o novo durante um período de transição, o que faz o código ficar mais sujo antes de ficar mais seguro. Pior ainda, quando um time decide que idempotência vale o investimento, geralmente é porque um incidente de processamento duplicado já aconteceu, e agora tem que somar à tarefa da feature um trabalho de limpeza para reconciliar os dados afetados.

Os times que lidam bem com isso constroem a chave de idempotência dentro do contrato da API desde o primeiro dia, mesmo antes de existir um incidente documentado que force a conversa. Custa um pouco mais de tempo durante o design inicial. Custa drasticamente menos tempo do que a alternativa.

Testar idempotência significa disparar a mesma requisição duas vezes de propósito

A maioria das suítes de teste verifica que um endpoint funciona. Quase nenhuma verifica que ele funciona corretamente quando chamado duas vezes com a mesma chave, ao mesmo tempo, antes de a primeira chamada terminar. Esse segundo cenário é o que realmente acontece em produção, e é o que um teste sequencial nunca vai pegar, porque um teste sequencial nunca cria a condição de corrida em primeiro lugar.

Testar isso direito significa escrever um teste que dispara duas requisições idênticas de forma concorrente e verifica que exatamente uma operação aconteceu, não que as duas requisições devolveram 200. Significa testar o que acontece quando uma chave é reutilizada depois que a transação original falhou no meio do caminho, porque "falhou" e "nunca aconteceu" precisam ser estados distinguíveis, não a mesma categoria. Significa testar o limite de expiração: o que acontece com uma requisição que chega com uma chave um segundo depois que sua janela de retenção fecha. Nenhum desses são casos extremos no sentido pejorativo. São os casos reais para os quais a idempotência existe, e pular eles nos testes significa descobrir se sua implementação funciona através de um incidente de produção em vez de através de uma rodada de testes.

O sinal de monitoramento que a maioria dos times não tem

Mesmo uma implementação correta de idempotência só faz o trabalho dela em silêncio se ninguém estiver olhando. A quantidade de requisições duplicadas que seu sistema deduplicou com sucesso é uma métrica que vale a pena acompanhar por si só, não só como ferramenta de debug depois que algo deu errado. Um pico repentino em requisições deduplicadas geralmente significa que algum sistema upstream começou a reenviar de forma mais agressiva, algo que vale a pena saber independente de sua camada de idempotência ter pego isso de forma limpa.

A ausência dessa métrica é um gap comum. Os times constroem a validação de idempotência, confirmam que funciona num teste manual, e seguem adiante sem instrumentá-la, o que significa que a primeira vez que alguém realmente olha com que frequência ela dispara é durante a revisão de um incidente, quando a pergunta "isso já estava acontecendo antes de hoje" não tem resposta. Um contador que soma cada vez que uma chave duplicada é pega custa quase nada para adicionar e transforma uma rede de segurança silenciosa em algo sobre o qual você realmente consegue raciocinar com o tempo.

Isso é uma conversa de design, não um detalhe de implementação

O motivo pelo qual idempotência separa engenheiros senior de qualquer um que consiga defini-la numa entrevista é que fazer isso corretamente exige uma decisão que a maioria prefere não tomar de forma explícita: quanta complexidade você está disposto a adicionar para prevenir um modo de falha que talvez aconteça raramente, mas custa dinheiro real ou confiança real quando acontece. Essa é uma conversa de tradeoffs, não um comentário de code review. Envolve o esquema do banco de dados, o design da API, a política de retries de cada sistema upstream que você não controla, e uma estimativa honesta de quão grave um duplicado realmente é no seu domínio específico.

Um "curtir" duplicado num post é um incômodo. Um pagamento duplicado é um reembolso, um chamado de suporte, e um cliente que a partir de agora checa duas vezes cada cobrança futura da sua empresa. Idempotência não é sobre tratar toda operação com a mesma paranoia. É sobre saber quais operações do seu sistema genuinamente não podem se dar ao luxo de rodar duas vezes, e ser deliberado o suficiente para garantir, no nível do banco de dados, que elas nunca vão.

ESCRITO POR

Logotipo de Howdy.com
Redacción Howdy.com
COMPARTILHAR