Idempotência: processar duas vezes sem estragar nada
A mesma mensagem vai chegar duas vezes: é matemática, não defeito. Idempotência natural, chaves com UNIQUE arbitrando a corrida e o teste que vale ouro: entregue 2x, afirme efeito 1x.
Eis a verdade desconfortável dos sistemas distribuídos: a mesma mensagem vai chegar duas vezes. Não é defeito do seu broker: é matemática. Entre entregar "no máximo uma vez" (e perder mensagens) e "pelo menos uma vez" (e ocasionalmente duplicar), toda mensageria séria escolhe a segunda. O timeout que expirou depois do processamento, o redrive da DLQ, o retry do cliente impaciente: as duplicatas têm mil pais.
A pergunta de engenharia não é "como evitar duplicatas?" (não dá); é "como processar a mesma mensagem N vezes com o efeito de uma?". Isso é idempotência, e é a propriedade que separa sistemas distribuídos confiáveis de geradores de estorno.
O estrago em uma linha
// Consumidor ingênuo: cada entrega duplicada cobra o cliente DE NOVO
async function processar(msg) {
await gateway.cobrar(msg.clienteId, msg.valor); // 💥
}
Caminho 1: idempotência natural (quando o domínio deixa)
Algumas operações já são idempotentes por natureza: setar um estado absoluto repete sem estrago.
// Não idempotente: cada execução soma de novo
await db.query("UPDATE conta SET saldo = saldo - $1", [valor]);
// Idempotente: repetir produz o mesmo estado final
await db.query("UPDATE pedido SET status = 'PAGO' WHERE id = $1", [id]);
A lição de desenho: sempre que puder, modele comandos como estados-alvo ("marque como pago") em vez de deltas ("subtraia X"). Boa parte da idempotência se ganha de graça na modelagem, como quase tudo em dados.
Caminho 2: a chave de idempotência (o padrão universal)
Quando a operação tem efeito cumulativo (cobrar, enviar, criar), o padrão é registrar um identificador único por operação e recusar repetições. O banco garante a atomicidade com uma constraint UNIQUE:
async function processar(msg) {
try {
// A "trava": só passa quem chega primeiro
await db.query(
"INSERT INTO operacoes_processadas (chave) VALUES ($1)",
[msg.idempotencyKey] // ex.: "pagamento:pedido-4193"
);
} catch (e) {
if (e.code === "23505") return; // duplicata: já processado, sai em paz
throw e;
}
await gateway.cobrar(msg.clienteId, msg.valor); // roda no máximo 1 vez
}
Três detalhes que separam o padrão correto do quase-correto:
- A chave nasce na origem, junto com a intenção ("pagamento do pedido 4193"), não no consumidor. Retry do produtor reusa a mesma chave.
- Verificar-depois-inserir tem corrida;
INSERTcom UNIQUE (ouINSERT ... ON CONFLICT DO NOTHING) não. Deixe o banco arbitrar. - Registro e efeito na mesma transação quando o efeito é no mesmo banco; quando é externo (gateway!), grave o registro antes e trate o caso "registrei mas falhei no meio" com reconciliação: melhor recusar uma cobrança legítima em retry do que cobrar duas vezes.
É exatamente o mecanismo que as APIs de pagamento expõem aos clientes: o header Idempotency-Key da Stripe é o exemplo canônico documentado (stripe.com/docs/api/idempotent_requests), e sua API pública de escrita merece o mesmo.

As perguntas frequentes de quem implementa
- Por quanto tempo guardar as chaves? Pela janela realista de duplicatas: retenção da fila + margem. 7 a 30 dias cobre a maioria; expire com TTL (o Redis serve bem aqui para janelas curtas, com o banco como registro durável para as críticas).
- E operações em lote? Uma chave por item, não por lote: o lote reprocessado pula os itens feitos e completa os pendentes.
- E a leitura? GET já é idempotente; a disciplina vale para escrita (POST especialmente). PUT bem desenhado (estado absoluto) volta ao caminho 1.
O teste que vale ouro
No seu conjunto de testes de integração, inclua o cenário: entregue a mesma mensagem duas vezes e afirme que o efeito aconteceu uma. É um teste barato que documenta a garantia e quebra na cara de quem remover a trava sem entender. Sistemas que passam nesse teste transformam duplicatas (inevitáveis) em não-eventos, e destravam o redrive sem medo.
Idempotência de ponta a ponta (das chaves na origem à reconciliação com terceiros) é parte do desenho assíncrono que fazemos no serviço de Arquitetura de Software.
Qual operação do seu sistema estragaria se rodasse duas vezes? Conte para a IA da Rabelo Digital, aqui no canto da tela, e ela desenha a trava certa para o caso. Sem formulário, sem espera.


Deixe um Comentário