Leonardo Peron5 min de leitura
Toda integração que funciona vai, mais cedo ou mais tarde, receber o mesmo aviso duas vezes. Não é anomalia: é como os dois lados combinaram de conversar. O provedor reenvia o webhook até receber confirmação, a sua rotina repete a chamada que estourou o tempo, alguém libera um lote parado. Em algum desses caminhos nasce o pedido gêmeo — mesmo cliente, mesmo valor, dois registros —, e ele costuma aparecer no financeiro, não no log.
Por que o evento chega duas vezes
O outro lado reenvia. Quase todo provedor repete a entrega até receber um 2xx. Se a sua resposta demorou e ele desistiu de esperar, do lado dele a entrega falhou — mesmo com o pedido já criado do lado de cá.
Nós reenviamos. A chamada estourou o tempo limite e a rotina tentou de novo. Tempo esgotado é a pior resposta possível: não diz se a operação aconteceu.
Alguém reprocessa. Uma fila parada é liberada, um lote é reenviado à mão, a varredura de reconciliação traz de volta o que o webhook já trouxe.
Há um quarto caso, que não é reenvio: dois processos consumindo a mesma fila pegam a mesma mensagem ao mesmo tempo. Nada chegou duas vezes; está sendo tratado duas vezes, em paralelo. Guarde a distinção, porque é ela que derruba a solução mais popular.
A chave identifica o fato, não a tentativa
Idempotência é a propriedade de uma operação que produz o mesmo resultado sendo executada uma ou dez vezes. Ela se obtém com uma chave, e a chave é a decisão inteira.
Ela tem que identificar o fato: o pedido 1234 da loja foi pago. Não a mensagem, não a tentativa, não o instante. O defeito clássico é gerar um identificador novo a cada envio — cada retentativa carrega uma chave inédita, e a proteção existe no código sem existir na prática. Pelo mesmo motivo, chave derivada do horário de chegada não serve: muda entre duas entregas do mesmo fato.
Vale dar escopo à chave: pedido 1234 da loja e pedido 1234 do marketplace são fatos diferentes com o mesmo número.
Onde a chave mora
O arranjo que parece prático e não é: guardar as chaves já vistas num cache em memória, ou num Redis, e gravar o pedido no banco. São duas escritas em dois lugares, e entre elas existe um intervalo. Um reinício ali dentro deixa a chave marcada sem o pedido, ou o pedido sem a marca. O primeiro caso é o silencioso: o pedido não existe e nada vai tentar criá-lo de novo.
A chave vai na mesma transação e no mesmo armazenamento do registro que ela protege, com restrição de unicidade na coluna.
"Verificar antes de inserir" tem corrida
Esta é a solução mais comum e mais frágil: consultar se o pedido já existe e, não existindo, inserir.
Entre a consulta e a inserção há uma janela. Dois consumidores da mesma fila consultam ao mesmo tempo, os dois não encontram nada, os dois inserem. A janela é curta, e é justamente esse o problema: ela só se manifesta sob concorrência, que é quando a integração está no pico. Em teste, com um evento por vez, o desenho passa sempre.
O banco resolve isso e o código não. Com índice único sobre a chave, a inserção duplicada é recusada pelo próprio banco, e o tratamento é capturar a violação e seguir como sucesso. A restrição é o único ponto do sistema onde verificar e escrever são a mesma operação.
Quando o destino é uma API de terceiro sem chave de idempotência — e alguns ERPs não oferecem —, a defesa fica antes da chamada, com a mesma restrição.
Guardar o resultado, não só a marca
Marcar a chave como vista e devolver sucesso vazio funciona enquanto ninguém precisa do retorno. Se quem chamou espera o identificador criado no destino, a segunda chamada devolve sucesso sem o número, e o chamador conclui alguma coisa errada — normalmente que precisa criar de novo. O desenho completo guarda chave e resultado: a repetição recebe de volta o mesmo resultado da primeira vez, sem executar nada.
Quando a origem não manda identificador estável
Acontece: o aviso chega sem identificador próprio, ou com um número novo a cada entrega. O caminho é montar a chave a partir do conteúdo, escolhendo os campos que identificam o fato — origem, tipo do evento, identificador do pedido e o estado que ele passou a ter. Escolher, não somar tudo: resumir a mensagem inteira num hash é frágil, porque qualquer campo de horário ou versão do formato produz um hash diferente, e a proteção para de funcionar sem avisar.
Existe o risco oposto, pior de perceber. Se dois fatos legítimos podem ser idênticos nos campos escolhidos — mesmo cliente, mesmo valor, mesmo minuto, duas compras de verdade —, a chave por conteúdo funde os dois num só. Duplicar é visível; engolir um pedido real não é. Quando os campos não separam os dois casos, é hora de pedir um identificador à origem em vez de adivinhar.
O teste que confirma
Dispare o mesmo webhook três vezes seguidas e confirme que existe um registro só. Isso é o mínimo, e quase toda integração passa.
O teste que separa é outro: dispare duas cópias ao mesmo tempo, em paralelo, não em sequência. É ele que exercita a corrida entre consultar e inserir, e é ele que a maioria dos desenhos não passa.
Confira também o código de resposta da segunda e da terceira entrega. Se voltaram como erro, o provedor vai continuar reenviando: a integração está certa no dado e presa num laço que ninguém pediu.