O carimbo na mão da festa
Na entrada da festa, sua mão é carimbada. Se você sair e voltar, o segurança vê o carimbo e não cobra o ingresso de novo — mesmo que você mostre o mesmo ticket duas vezes. A inbox é o carimbo: o consumidor marca 'esta mensagem eu já processei', e quando ela reaparece (porque a rede reenviou), ele a reconhece e deixa passar sem refazer o trabalho. Sem o carimbo, cada reentrada seria cobrada de novo.
O detalhe que separa o certo do quase-certo
Registrar o id e executar o efeito parecem dois passos, e tratá-los como dois passos é o erro. Se você registra o id e cai antes de aplicar o efeito, a mensagem fica marcada como processada sem ter sido — e o reenvio será ignorado, então ela some para sempre.
A saída é fazer os dois de forma atômica: registrar o id e aplicar o efeito na mesma transação do banco. Ou os dois acontecem, ou nenhum — e a duplicata que chegar depois encontra o id já lá e é descartada com segurança.
A implementação mais robusta nem separa checar de inserir: um INSERT do id numa coluna com restrição de unicidade, com ON CONFLICT DO NOTHING, faz o próprio banco decidir. Se o insert afetou uma linha, é a primeira vez — processe; se afetou zero, é duplicata — ignore. Isso elimina a corrida do 'checar se existe e depois inserir', que sob concorrência deixaria duas cópias passarem (o clássico check-then-act de uma condição de corrida).
A inbox é a metade consumidora de um par com o Outbox: o Outbox garante que o produtor não perca o evento (grava a intenção de publicar na mesma transação do dado); a inbox garante que o consumidor não o processe duas vezes. Juntos, eles dão o efeito prático de 'exactly-once' — não porque a rede entregou exatamente uma vez (ela não entrega), mas porque o produtor não perde e o consumidor deduplica. É assim que se contorna o fato de que entrega exatamente-uma-vez de ponta a ponta é impossível: você move a garantia do transporte para as pontas.
Como isso aparece na prática
O cartão cobrado duas vezes
Cenário
Um consumidor de eventos de pagamento cobrava o cartão ao receber a mensagem. Numa reentrega após um timeout de rede (a mensagem tinha dado certo, mas o ack se perdeu), alguns clientes foram cobrados duas vezes.
Como o padrão entra
O consumidor passou a registrar o id do evento numa inbox com unicidade, na mesma transação da cobrança. A reentrega passou a encontrar o id já registrado e a ser ignorada.
Trade-off: Cada evento ganhou uma escrita a mais e uma tabela de inbox a manter e limpar. Em troca, a cobrança dupla — que virava estorno, suporte e desconfiança — deixou de acontecer.
A janela de dedup do broker que expirou
Cenário
Um sistema confiava só na deduplicação nativa do broker, com janela de cinco minutos. Uma mensagem represada por uma falha longa foi reprocessada horas depois, já fora da janela, e o efeito duplicou.
Como o padrão entra
A deduplicação passou a ser durável na inbox da aplicação, sem janela de expiração para os ids que importam, complementando a do broker em vez de depender só dela.
Trade-off: A inbox cresce e precisa de uma política de retenção pensada. Em troca, a deduplicação deixou de ter um prazo de validade que ninguém tinha percebido que existia.
Onde isto dá errado
Registrar o id fora da transação do efeito
Se marcar 'processado' e aplicar o efeito são passos separados, uma falha entre eles ou perde o efeito (marcou, não aplicou) ou duplica (aplicou, não marcou). Os dois têm que ser atômicos, na mesma transação.
Checar-e-inserir em vez de restrição de unicidade
'Consultar se o id existe e depois inserir' é um check-then-act: sob concorrência, duas cópias checam ao mesmo tempo, ambas não encontram e ambas processam. A unicidade no banco resolve a corrida de uma vez.
Confiar só na janela de dedup do broker
A deduplicação do broker costuma valer por uma janela de tempo. Uma mensagem atrasada além dela reaparece como nova. Para o que não pode duplicar nunca, a inbox durável na aplicação é o que segura.
Registrar o id e aplicar o efeito, juntos
// Entrega "ao menos uma vez" significa: a mesma mensagem PODE
// chegar duas vezes (reenvio apos falha, timeout que era sucesso).
// A inbox e a memoria do consumidor: "ja processei este id?".
async function processar(msg: { id: string; payload: unknown }) {
// 1. tenta registrar o id numa tabela com UNIQUE constraint
const novo = await inbox.registrarSeNovo(msg.id); // INSERT ... ON CONFLICT
if (!novo) return; // ja processado: ignora a duplicata, em silencio
// 2. so agora executa o efeito, na MESMA transacao do registro
await efeitoDeNegocio(msg.payload);
}
// O detalhe que separa o certo do quase-certo: registrar o id e
// aplicar o efeito precisam ser ATOMICOS. Se registrar e depois
// cair antes do efeito, a mensagem some (registrada, nao aplicada).
// Por isso os dois vao na mesma transacao do banco.Quando usar × quando evitar
Use quando
3- Consumidores cujo efeito não é idempotente por natureza.
- Quando o broker entrega ao menos uma vez e a duplicata dói.
- Como par do Outbox, para o efeito prático de exactly-once.
Evite quando
2- Quando o efeito já é idempotente (um UPSERT, por exemplo).
- Quando duplicar o efeito é inofensivo e a inbox não se paga.
Testar o que ficou
3 armadilhas de Inbox e deduplicação viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
tabela eventos_processadosUma tabela que guarda os ids já vistos, consultada antes de processar, é a forma mais comum de implementar a inbox.INSERT ... ON CONFLICT DO NOTHINGA restrição de unicidade no id da mensagem faz o banco recusar a duplicata sozinho, sem corrida entre checar e inserir.dedup do SQS FIFO / KafkaBrokers oferecem deduplicação por id numa janela de tempo, mas ela expira — a inbox durável cobre o que a janela do broker não alcança.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Já processei este messageId? Ignora.
if (await inbox.jaViu(msg.id)) return;O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Guardar e consultar os ids já processados custa armazenamento e uma leitura ou escrita por mensagem
- Definir a janela de retenção dos ids é um trade-off entre memória e a chance de reprocessar um antigo
Não vale se: o processamento já é naturalmente idempotente — se aplicar a mensagem duas vezes não muda o resultado, a inbox é redundante.
Onde isto aparece
Continue por aqui
- IdempotênciaExecutar a mesma operação duas vezes tem o mesmo efeito de executar uma: é o que torna o retry seguro. Em rede, onde 'não sei se foi' é resposta comum, é a diferença entre reenviar e cobrar em dobro.
- Garantias de entregaAt-most-once perde, at-least-once duplica, e exactly-once não existe entre sistemas distintos. Entender qual das três você tem — e por que a terceira é vendida sem ser verdade — é o que decide se o cliente vai ser cobrado duas vezes.
- Transactional OutboxSalvar no banco e publicar um evento são duas operações em dois sistemas — e não há transação que cubra as duas. O outbox transforma o evento numa linha da mesma transação, e deixa a publicação para depois.