Outbox: O Jeito Menos Glamoroso de Não Perder Evento
Trabalheo em um projeto que tinha um bug que só aparecia quando a sorte era ruim.
Fluxo assim: API recebe pedido de compra, debita saldo no PostgreSQL, publica evento no RabbitMQ pro serviço de transações processar. Funcionava nos testes. Funcionava na demo. Uma vez em dez, o dinheiro saía da conta e a mensagem não chegava.
Commit no banco tinha ido. Publish na fila falhou. Timeout, rede, broker reiniciando. O usuário ficava com saldo inconsistente e nenhum log claro do outro lado.
Dual write na mão é armadilha clássica. Demorei a aceitar.
A tentativa ingênua:
tx.Begin()
tx.Exec("UPDATE users SET balance = balance - $1 WHERE id = $2", amount, userID)
publisher.Publish("transactions", event)
tx.Commit()
Se Publish falha depois do Exec mas antes do Commit, você faz rollback e respira. Se Commit passa e Publish falha depois, você tem problema. Se Publish passa e Commit falha, mensagem fantasma na fila.
Não existe transação distribuída gratuita entre Postgres e RabbitMQ. Alguém precisa ser fonte da verdade.
A saída que adotamos: tabela outbox no mesmo Postgres.
API PostgreSQL Worker
| | |
|-- BEGIN ---------------->| |
|-- UPDATE balance ------->| |
|-- INSERT outbox -------->| |
|-- COMMIT --------------->| |
| |<-- poll pending -------|
| | |-- publish RabbitMQ
| |<-- mark sent ----------|
Na mesma transação ACID:
- Atualiza saldo.
- Insere linha na
outboxcom payload JSON do evento e statuspending. - Commit.
Worker separado lê pending, publica no RabbitMQ, marca sent. Se publish falha, tenta de novo. Se worker morre, evento continua na outbox esperando.
O broker nunca participa da transação do dinheiro. Só recebe cópia depois que o banco garantiu.
CREATE TABLE outbox (
id UUID PRIMARY KEY,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Outbox adiciona atraso. Evento não é instantâneo, passa pelo worker. Precisa monitorar fila de outbox pendente, lidar com poison message, decidir retenção.
Pra sistema financeiro de faculdade, atraso de alguns segundos era aceitável. Pra notificação em tempo real talvez não.
Também tem variantes: CDC com Debezium lendo WAL, transactional inbox do lado consumer. Outbox manual foi o que coube no prazo do semestre.
Antes desse projeto eu achava que mensageria resolvia desacoplamento sozinha. Desacopla serviço, não resolve consistência.
Outbox não é pattern glamoroso. Ninguém coloca no slide bonito de arquitetura. Mas é o tipo de decisão que separa protótipo de sistema que sobrevive segunda-feira com broker instável.
Fora isso… Obrgiado pela atenção, um abraço e até mais!
j