Parte IV — Confiabilidade
Idempotência
Por que consumers precisam ser idempotentes, e como implementar isso com eventId, constraint de unicidade e atomicidade.
Nesta página
O Capítulo 10 terminou apontando para cá: At Least Once — a garantia mais usada em sistemas financeiros — aceita duplicidade como possibilidade estrutural. Este capítulo mostra como um consumer absorve essa duplicidade sem gerar efeitos colaterais duplicados.
O que é idempotência
Idempotência
Um processamento é idempotente quando produz o mesmo efeito final, independentemente de ser executado uma ou várias vezes com a mesma entrada. Para um consumer Kafka, isso significa: processar o mesmo evento duas vezes não deve gerar o dobro do efeito — o saldo é creditado uma vez, o e-mail é enviado uma vez, o registro existe uma vez.
Por que consumers precisam ser idempotentes
Duplicidade não é uma possibilidade remota
At Least Once (Capítulo 10) garante que nenhuma mensagem é perdida, aceitando como preço que ela pode ser entregue mais de uma vez — depois de um rebalance (Capítulo 6), de um retry de commit, ou de um crash entre o fim do processamento e o commit do offset (Capítulo 7). Isso não é um bug raro: é o comportamento esperado e documentado dessa garantia. Um consumer que não trata isso vai duplicar efeitos em produção, mais cedo ou mais tarde.
O padrão: eventId + constraint de unicidade
O padrão mais comum e mais robusto: cada evento carrega um identificador único (eventId, geralmente um
UUID gerado pelo producer no momento da criação do evento). O consumer mantém uma tabela
(eventos_processados, ou similar) com uma constraint de unicidade nesse eventId. Antes de aplicar o
efeito de negócio, o consumer tenta inserir o eventId nessa tabela; se a inserção falhar por violação de
unicidade, o evento já foi processado e a operação é ignorada com segurança.
Atomicidade: a parte que costuma ser esquecida
"Eu checo se o eventId já existe antes de processar"
Checar e processar em passos separados (uma consulta SELECT, depois um INSERT/UPDATE de negócio)
introduz uma condição de corrida: sob concorrência (dois consumers, ou reprocessamento simultâneo), ambos
podem checar "não existe" ao mesmo tempo e aplicar o efeito duas vezes antes que qualquer um grave o
eventId. A checagem e a aplicação do efeito precisam acontecer na mesma transação de banco, com a
constraint de unicidade como a garantia real — não uma checagem prévia em código de aplicação.
Na prática, isso significa: inserir o eventId na tabela de controle e aplicar o efeito de negócio (debitar,
creditar, gravar) na mesma transação. Se a constraint de unicidade rejeitar a inserção, a transação inteira
falha e é revertida — nenhum efeito parcial é aplicado, mesmo sob concorrência.
eventId vs. transactionId
Duas chaves de deduplicação diferentes
| Chave | Identifica | Uso típico |
|---|---|---|
eventId | O evento Kafka específico (uma publicação) | Deduplicar reentrega do mesmo evento (retry, rebalance) |
transactionId (ou ID de negócio) | A transação de domínio (ex.: o PIX, o pagamento) | Deduplicar a operação de negócio, mesmo se vier de eventos diferentes |
Normalmente eventId é suficiente para lidar com reentrega do Kafka. Mas em cenários onde o mesmo fato de
negócio pode chegar por caminhos diferentes (ex.: uma notificação do banco duplicada pelo canal de origem,
gerando dois eventos Kafka distintos para a mesma transação real), a deduplicação por eventId sozinha não
basta — é necessário também um identificador de negócio único (transactionId, endToEndId no caso de
PIX) para pegar essa duplicidade de origem, antes mesmo de chegar ao Kafka.
Quando idempotência natural dispensa a tabela de controle
Nem todo consumer precisa da tabela eventos_processados. Se a operação já é naturalmente idempotente —
um upsert que sempre grava o mesmo valor final, independentemente de quantas vezes for executado — o
controle explícito de eventId é redundante.
Upsert de score de crédito
O risco-service grava o score mais recente de cada cliente com um UPDATE ... WHERE clienteId = ?
(upsert). Processar o mesmo evento duas vezes simplesmente grava o mesmo valor duas vezes — sem efeito
colateral adicional. Esse consumer é naturalmente idempotente e não precisa de controle de eventId.
Já operações que incrementam ou acumulam estado (creditar saldo, incrementar contador, enviar
notificação) não são naturalmente idempotentes — processá-las duas vezes duplica o efeito, e por isso
exigem o controle explícito de eventId.
Como aparece em entrevistas
"Como você evitaria processamento duplicado?" é praticamente garantida sempre que At Least Once entra na conversa. A resposta fraca menciona apenas "eventId único". A resposta forte explica a atomicidade: checagem e aplicação do efeito na mesma transação, com constraint de unicidade como garantia real sob concorrência — não uma checagem prévia em código.
Dica de entrevista
Ao descrever idempotência, sempre mencione a constraint de unicidade e a atomicidade da transação. É esse
detalhe — não a existência de um eventId — que separa uma implementação correta de uma que ainda tem
condição de corrida sob concorrência.
Relação com Java e Spring Boot
Em Spring Boot, isso tipicamente se traduz em uma tabela com uma coluna event_id marcada UNIQUE, e o
código de processamento dentro de um método anotado com @Transactional, que tenta inserir o registro de
controle e aplicar o efeito de negócio antes de qualquer commit. Se a inserção lançar uma
DataIntegrityViolationException (violação da constraint), o código captura essa exceção especificamente e
trata como "já processado" — não como um erro genérico a ser propagado para o mecanismo de retry
(Capítulo 9).
Crédito de PIX idempotente
@Transactional
public void creditarPix(PixRecebidoEvent evento) {
try {
eventosProcessadosRepository.insert(evento.getEventId());
} catch (DataIntegrityViolationException e) {
return; // já processado, nada a fazer
}
contaRepository.creditar(evento.getContaId(), evento.getValor());
}
A inserção do eventId e o crédito do saldo acontecem na mesma transação — se a inserção falhar por
duplicidade, o crédito nunca é aplicado uma segunda vez.
Resumo
Idempotência é o que permite um consumer absorver a duplicidade estrutural do At Least Once sem duplicar
efeitos de negócio. O padrão central é um eventId único, verificado e inserido na mesma transação que
aplica o efeito — a constraint de unicidade do banco, não uma checagem prévia em código, é o que garante
atomicidade sob concorrência. Operações naturalmente idempotentes (upsert) podem dispensar esse controle;
operações que acumulam estado (creditar, incrementar) não podem.
Pode vir a seguir
Prováveis follow-ups: "o que você faria se o mesmo evento de negócio chegasse por dois eventId
diferentes?" e "como a transactional outbox se relaciona com idempotência?" (Capítulo 12).