Exactly-Once no Node.js é uma garantia frequentemente desejada em sistemas distribuídos: cada mensagem produziria seu efeito uma única vez. Na prática, redes falham, acknowledgements se perdem e processos caem entre o efeito e a confirmação.
Por isso, sistemas confiáveis normalmente combinam entrega pelo menos uma vez com processamento idempotente. Algumas plataformas oferecem transações em limites específicos, mas nenhum recurso elimina a necessidade de definir claramente qual efeito deve ser único.
Entrega e processamento
“Mensagem entregue uma vez” e “efeito aplicado uma vez” são conceitos diferentes. O broker pode reenviar uma mensagem, enquanto a aplicação impede a duplicação no banco.
A janela de falha
await cobrarCartao(event);
await confirmarMensagem(event);Se o processo cair depois da cobrança e antes da confirmação, o broker reenviará. Sem uma idempotency key, pode ocorrer cobrança duplicada.
At-most-once
Na entrega no máximo uma vez, a mensagem pode ser perdida, mas não é repetida. É aceitável para métricas aproximadas, porém inadequado para operações financeiras ou estoque.
At-least-once
Na entrega pelo menos uma vez, a mensagem não deve ser perdida, mas pode chegar novamente. Esse modelo é comum e exige idempotência.
Exactly-once dentro de um banco
Quando o registro da mensagem e o efeito usam o mesmo banco, uma transação local pode garantir aplicação única.
await db.transaction(async trx => {
const inserted = await trx.query(
`INSERT INTO inbox (message_id, processed_at)
VALUES ($1, now())
ON CONFLICT DO NOTHING
RETURNING message_id`,
[event.id]
);
if (inserted.rowCount === 0) return;
await trx.query(
'UPDATE contas SET saldo = saldo + $1 WHERE id = $2',
[event.data.valor, event.data.contaId]
);
});O efeito e a deduplicação são confirmados juntos.
Limites da transação
Se o handler também envia e-mail, chama gateway e grava em outro banco, a transação local não cobre tudo. Cada efeito externo precisa de estratégia própria.
Idempotency Keys
APIs externas podem aceitar uma chave estável.
await gateway.cobrar({
valor,
idempotencyKey: `payment:${event.id}`
});O provedor deve armazenar o resultado por chave e devolver a mesma resposta em retries.
Outbox
Para publicar eventos após uma mudança local, grave uma Outbox na mesma transação. O envio pode repetir, mas o evento possui ID estável.
Leia Outbox Pattern no Node.js.
Kafka Transactions
Kafka oferece transações para coordenar leitura, produção e offsets dentro do próprio ecossistema. Isso ajuda em pipelines Kafka-to-Kafka, mas não torna uma chamada HTTP ou gravação externa automaticamente exactly-once.
Deduplicação
A tabela Inbox deve ter chave única e retenção compatível com a possibilidade de redelivery. Apagar IDs cedo demais permite duplicatas antigas.
Chave de negócio
Às vezes o ID da mensagem muda durante retry. Uma chave de negócio pode ser mais adequada, como pedidoId + tipoDaOperação.
UNIQUE (order_id, operation_type)Operações naturalmente idempotentes
Definir estado absoluto é mais fácil de repetir do que aplicar incremento.
// Mais seguro para repetição
UPDATE pedidos SET status = 'confirmado' WHERE id = $1;
// Exige deduplicação
UPDATE estoque SET quantidade = quantidade - 1 WHERE sku = $1;Comutatividade
Algumas operações podem ser desenhadas para produzir o mesmo resultado independentemente da ordem, usando conjuntos, versões e valores máximos.
Concorrência
Duas cópias da mesma mensagem podem chegar simultaneamente. A proteção precisa estar no banco, não apenas em um Set em memória.
Resposta armazenada
Para endpoints idempotentes, salve status, fingerprint e resposta original.
if (registro.completed) {
return registro.response;
}Reusar a mesma chave com payload diferente deve gerar conflito.
Observabilidade
Meça duplicatas detectadas, conflitos de chave, tempo de retenção da Inbox, falhas após efeitos externos e retries por operação.
Testes de falha
- queda antes do efeito;
- queda depois do efeito e antes do ack;
- duas entregas simultâneas;
- retry com mesma chave e payload;
- mesma chave com payload diferente;
- redelivery após retenção;
- timeout da dependência com resultado desconhecido.
Resultado desconhecido
Um timeout não significa que a operação falhou. O servidor remoto pode ter concluído. Consulte o estado usando a mesma chave antes de executar novamente.
Erros comuns
- afirmar exactly-once sem definir o limite;
- deduplicação apenas em memória;
- Inbox fora da transação;
- chave aleatória em cada retry;
- não armazenar fingerprint;
- ignorar efeitos externos;
- retenção curta;
- confundir ack com conclusão única.
Conclusão
Exactly-Once no Node.js deve ser tratado como propriedade de um efeito dentro de um limite específico, não como promessa global do sistema. O caminho mais robusto é aceitar redelivery e tornar cada operação idempotente.
Use transações locais, Inbox, Outbox, chaves estáveis e contratos claros com serviços externos. Consulte Idempotency Keys no Node.js e Event-Driven Architecture no Node.js.
Leia a documentação de garantias de entrega do Apache Kafka.


