Consumer Idempotente no Node.js processa a mesma mensagem mais de uma vez sem duplicar o efeito de negócio. Essa propriedade é essencial porque brokers podem reenviar mensagens quando um processo cai, perde o acknowledgement ou sofre rebalanceamento.
Idempotência não significa ignorar qualquer repetição. O consumidor precisa identificar a operação, proteger concorrência e devolver resultado consistente.
Por que duplicatas acontecem?
- queda depois do commit e antes do ack;
- timeout de rede;
- rebalanceamento de partições;
- retry do produtor;
- reprocessamento manual;
- redelivery após indisponibilidade.
Inbox Pattern
Uma tabela Inbox registra mensagens processadas.
CREATE TABLE inbox (
consumer text NOT NULL,
message_id text NOT NULL,
processed_at timestamptz NOT NULL,
PRIMARY KEY (consumer, message_id)
);Deduplicação atômica
async function processar(event) {
await db.transaction(async trx => {
const result = await trx.query(
`INSERT INTO inbox (consumer, message_id, processed_at)
VALUES ($1, $2, now())
ON CONFLICT DO NOTHING
RETURNING message_id`,
['billing.order-created.v1', event.id]
);
if (result.rowCount === 0) return;
await faturas.criar({
pedidoId: event.data.orderId,
total: event.data.totalCents
}, trx);
});
}O registro Inbox e o efeito usam a mesma transação.
Concorrência
Duas instâncias podem receber a mensagem ao mesmo tempo. A chave única do banco decide qual venceu. Um cache em memória não oferece essa proteção distribuída.
Chave da mensagem
Use ID estável gerado pelo produtor. Se retries criarem IDs novos, deduplique por chave de negócio.
const operationKey = `create-invoice:${event.data.orderId}`;A chave deve representar o efeito que precisa ser único.
Fingerprint
Ao usar idempotency key, salve hash do payload. A mesma chave com dados diferentes deve gerar erro.
const fingerprint = createHash('sha256')
.update(JSON.stringify(normalize(event.data)))
.digest('hex');Operações idempotentes por natureza
Definir um valor absoluto costuma ser idempotente:
UPDATE pedidos SET status = 'confirmado' WHERE id = $1;Incrementos e inserções exigem proteção adicional.
Upsert
INSERT INTO projeção_pedidos (id, status, version)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status,
version = EXCLUDED.version
WHERE projeção_pedidos.version < EXCLUDED.version;A versão também protege contra eventos fora de ordem.
Efeitos externos
Enviar e-mail, cobrar cartão ou chamar API não participa da transação local. Use idempotency key no fornecedor ou grave uma Outbox para executar o efeito de forma recuperável.
Estado de processamento
Operações longas podem registrar processing, completed e failed. Defina lease ou timeout para recuperar registros abandonados.
Retenção da Inbox
Não apague IDs antes da janela máxima de redelivery e replay. A retenção pode variar por tópico e criticidade.
Payload inválido
Valide schema antes do efeito. Eventos incompatíveis devem ir para DLQ, não ser marcados como sucesso silenciosamente.
Acknowledgement
Confirme a mensagem apenas depois do commit. Se o ack falhar, haverá redelivery, mas a Inbox impedirá duplicação.
Retries
Em erro transitório, faça rollback para que a mensagem possa ser tentada novamente. Em erro permanente, registre contexto e mova para DLQ.
Observabilidade
Monitore duplicatas detectadas, mensagens em processamento, conflitos de fingerprint, duração, retries e tamanho da Inbox.
Testes
- mesma mensagem duas vezes;
- duas entregas simultâneas;
- queda após commit;
- rollback por erro transitório;
- mesma chave com payload diferente;
- evento fora de ordem;
- replay após retenção;
- efeito externo com timeout.
Consumer e projeções
Projeções podem usar versão do aggregate para aplicar apenas eventos mais novos. Isso complementa a deduplicação por ID.
Consumer e Saga
Handlers de saga também precisam ser idempotentes. Uma resposta repetida não deve avançar o estado duas vezes.
Leia Saga Pattern no Node.js.
Erros comuns
- deduplicação em memória;
- Inbox e efeito em transações diferentes;
- ack antes do commit;
- ID instável;
- não tratar concorrência;
- apagar Inbox cedo;
- ignorar efeito externo;
- marcar erro permanente como sucesso sem DLQ.
Conclusão
Consumer Idempotente no Node.js transforma redelivery em comportamento esperado, em vez de incidente. A base é uma identidade estável, proteção no banco e efeito atômico.
Use Inbox, chaves de negócio, fingerprints e versões. Combine com Exactly-Once no Node.js, Dead Letter Queue no Node.js e Idempotency Keys no Node.js.
Consulte a referência do padrão Idempotent Consumer.


