01
Por que gravar e publicar são duas escritas, e uma sempre pode falhar sozinha
O banco e o broker são sistemas diferentes, com processos, redes e falhas diferentes. Não existe COMMIT que cubra os dois. Qualquer sequência que você escolha deixa uma janela em que um lado foi feito e o outro não: um deploy no meio da requisição, uma queda de rede, um timeout do broker, um erro de serialização, um OOM kill. Essa janela dura milissegundos, e por isso o problema passa em teste e aparece quando o volume e a quantidade de deploys sobem. Com 1.200 pedidos por dia e um incidente de infraestrutura por mês, é questão de tempo.
Inverter a ordem não resolve, só troca o sintoma. Publicar antes de gravar cria um evento sobre algo que não existe: o faturamento emite nota de um pedido que o banco recusou. Gravar antes de publicar cria um dado sem evento: o pedido está confirmado e ninguém foi avisado. Publicar dentro da transação, antes do COMMIT, é o pior dos mundos porque parece seguro: se o COMMIT falhar depois, o evento já saiu.
| Estratégia | Falha entre os dois passos | Resultado | Detectável? |
|---|---|---|---|
| Gravar e depois publicar | Processo cai ou broker recusa após o COMMIT | Pedido confirmado sem evento | Só na conciliação, dias depois |
| Publicar e depois gravar | Banco recusa ou processo cai após o publish | Evento de um pedido que não existe | Quando o consumidor não acha o pedido |
| Publicar dentro da transação | COMMIT falha após o publish | Evento de uma gravação desfeita | Raramente |
| Retry em memória após o COMMIT | Processo reinicia com a fila de retry na memória | Pedido confirmado sem evento | Só na conciliação |
| Outbox transacional | Qualquer um | Evento publicado no mínimo uma vez, ou transação desfeita por inteiro | Sim, a tabela mostra o atraso |
Escrita dupla (o que quebra) Outbox (o que segura)
app -- UPDATE pedidos ----------> banco app -- BEGIN ---------------------> banco
app -- publish(pedido.confirmado) -> broker |-- UPDATE pedidos
^ |-- INSERT outbox (evento)
| '-- COMMIT (os dois, ou nenhum)
queda aqui = pedido gravado, evento perdido
ordem inversa = evento publicado, pedido nao relay -- SELECT ... FOR UPDATE SKIP LOCKED
|-- publish(evento) ----------> broker
'-- UPDATE outbox SET publicado_em
consumidor -- INSERT inbox (message_id) ON CONFLICT
'-- trata o evento so se for a primeira vez