O pedido foi salvo, mas o evento que deveria reservar o estoque nunca chegou ao broker. O processo caiu entre o COMMIT e a chamada de publicação. No caminho inverso, publicar primeiro pode anunciar um pedido que depois falha no banco.
O padrão outbox transacional fecha essa janela gravando a mudança de negócio e o evento em uma única transação local. Depois do commit, um relay lê a tabela de outbox e publica as linhas pendentes. A publicação ainda pode falhar ou acontecer duas vezes, então o desenho precisa incluir retry, observabilidade e consumidor idempotente.
Este é um spoke da execução durável para agentes de IA com checkpoints, filas e workflows. O padrão também serve para APIs e serviços comuns. Em um runtime de agente, a mesma fronteira aparece quando uma transição confirmada precisa acordar uma tarefa, emitir um evento de auditoria ou iniciar uma etapa fora do processo atual. O outbox não torna a chamada externa parte da transação do banco.

Resposta curta
- Grave o registro de negócio e a mensagem do outbox na mesma transação do banco.
- Publique depois do commit por polling ou CDC, mantendo a linha até a entrega ser aceita pelo relay.
- Considere duplicatas normais: o relay pode cair depois de publicar e antes de registrar o sucesso.
- Faça o consumidor deduplicar por um ID estável e preserve a ordem apenas onde o domínio realmente exige.
- Prefira uma transação local simples quando não existe uma segunda obrigação de publicação.
O problema é um dual write
Imagine este código em uma API de pedidos:
const order = await saveOrder(input);
await broker.publish("OrderPlaced", order);
return order;
Se saveOrder confirmar e o processo morrer antes de publish, o banco conhece o pedido, mas o estoque, o e-mail ou a projeção downstream não recebem a notícia. Se a ordem for invertida, o evento pode chegar enquanto o registro não existe. Um retry também não resolve sozinho: um timeout pode esconder uma publicação que já foi aceita.
O AWS Prescriptive Guidance descreve esse caso como uma operação de escrita em dois sistemas. A recomendação é persistir o evento junto com a atualização e usar um processo separado para enviá-lo. O padrão de Transactional Outbox do microservices.io chega à mesma fronteira: a transação local confirma a obrigação; o relay cuida da comunicação posterior.
Isso não é uma promessa de entrega exatamente uma vez. É uma forma de evitar que a obrigação de publicar exista apenas na memória do processo.
Quando o outbox vale o custo?
Use o padrão quando uma mudança local confirmada precisa causar um efeito assíncrono em outro sistema e perder essa notificação deixa estados divergentes. Exemplos:
| Situação | Decisão inicial |
|---|---|
| Atualizar uma tabela e chamar um broker em seguida | Considere o outbox na mesma transação |
| Salvar uma preferência sem nenhuma notificação downstream | Uma transação local costuma bastar |
| Criar uma tarefa de agente depois de uma aprovação persistida | Outbox pode registrar a obrigação de iniciar a tarefa |
| Atualizar vários bancos como se fosse uma única transação | O outbox não resolve o commit entre serviços; avalie uma saga ou outro desenho de coordenação |
| Enviar e-mail diretamente após o commit | O outbox pode proteger a entrega da intenção, mas o provedor de e-mail ainda precisa de idempotência ou de um recibo seguro |
O custo é real. Você passa a operar uma tabela, um relay, retries, retenção, métricas e uma política para mensagens que não avançam. Se a operação pode continuar síncrona dentro do mesmo banco, introduzir um broker e um relay pode adicionar partes sem resolver um problema existente.
O que precisa ser confirmado junto?
A unidade atômica deve conter o estado que o leitor downstream precisa e a obrigação de publicar a mudança. Em PostgreSQL, COMMIT torna as alterações da transação visíveis e duráveis quando a operação retorna sucesso (documentação do PostgreSQL sobre COMMIT). O formato exato depende do banco e do driver, mas a ideia é a mesma:
-- Forma ilustrativa. Adapte tipos, constraints e cliente ao seu banco.
CREATE TABLE outbox_events (
id uuid PRIMARY KEY,
aggregate_type text NOT NULL,
aggregate_id text NOT NULL,
event_type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL,
published_at timestamptz,
attempts integer NOT NULL DEFAULT 0,
last_error text
);
BEGIN;
UPDATE orders
SET status = 'placed'
WHERE id = 'order-42';
INSERT INTO outbox_events
(id, aggregate_type, aggregate_id, event_type, payload, created_at)
VALUES
('event-42', 'order', 'order-42', 'OrderPlaced', '{"orderId":"order-42"}', now());
COMMIT;
O UPDATE e o INSERT precisam falhar ou confirmar juntos. Se a transação fizer rollback, o relay não deve encontrar uma obrigação fantasma. Se fizer commit, a linha do outbox precisa carregar um ID estável, o tipo do evento, a chave do agregado, o payload e informação suficiente para diagnosticar tentativas.
Não grave no outbox em uma conexão separada por acidente. Isso recria o dual write: o pedido pode confirmar enquanto a linha do evento falha, ou o contrário. Passe o mesmo contexto transacional ao código que salva a entidade e ao código que registra o evento.
O relay publica depois do commit
O relay é um worker, não uma etapa invisível da transação. Ele encontra linhas elegíveis, reclama um lote, publica no broker e registra o resultado. Se a publicação falhar, preserva a linha, registra o erro e agenda uma nova tentativa. Se o processo morrer depois do broker aceitar a mensagem e antes de gravar published_at, a mesma linha pode ser tentada novamente.
Para vários relays, a tabela precisa de uma política de claim. Em PostgreSQL, FOR UPDATE SKIP LOCKED pode evitar que workers aguardem as mesmas linhas, mas a própria documentação avisa que o resultado não é uma visão consistente e que esse recurso serve para tabelas parecidas com filas, não para qualquer consulta (documentação do PostgreSQL sobre locking em SELECT). O claim também precisa de lease ou recuperação quando um worker some no meio do publish.
Há duas famílias comuns de relay:
- Polling publisher: workers consultam a tabela em intervalos ou recebem um sinal para acordar. É simples de entender, mas precisa de índices, limites de lote, backoff e uma política para não transformar a tabela de negócio em um gargalo.
- Change data capture: uma ferramenta captura a inserção na tabela e a encaminha ao broker. O Outbox Event Router do Debezium, por exemplo, lê mudanças de uma tabela de outbox e roteia eventos por campos do registro. CDC reduz o polling da aplicação, mas adiciona configuração, operação e uma dependência de captura.
Escolha pela carga, latência aceitável, capacidade operacional, ordenação necessária e caminho de recuperação. Não há uma taxa universal que diga quando trocar um relay por outro.
Duplicatas e ordem continuam sendo responsabilidade do desenho
O relay não pode apagar a linha antes de uma publicação aceita. Mesmo depois de marcar o evento como publicado, o processo pode ter caído no intervalo entre o broker e o banco. Por isso, a AWS recomenda consumidores idempotentes para mensagens duplicadas, e o padrão descrito pelo microservices.io trata a publicação duplicada como uma possibilidade normal do relay.
O consumidor pode usar o ID do evento em uma tabela de inbox com constraint única, ou pode fazer a própria operação ser idempotente. O importante é que a verificação e o efeito não criem uma nova corrida. Um SELECT de verificação seguido de um INSERT separado não protege nada se duas entregas chegarem juntas; use uma constraint ou uma transação adequada ao banco.
A ordem também precisa de escopo. O AWS alerta que eventos devem respeitar a ordem das atualizações do serviço quando essa sequência é necessária para o domínio. Isso não significa que todas as mensagens de todos os agregados precisem de uma fila global. Escolha uma chave de ordenação por agregado, partição ou fluxo, e documente o que um consumidor pode fazer com um evento atrasado.
Como verificar o contrato antes de operar
Um outbox não está pronto quando a tabela existe. Verifique os casos que atravessam o processo inteiro:
- Rollback: force uma falha depois de inserir a entidade e o evento. Nenhuma das duas linhas deve permanecer confirmada.
- Relay indisponível: confirme que o estado local continua recuperável e que a linha não some quando o broker recusa a conexão.
- Timeout ambíguo: simule uma publicação aceita seguida de erro no relay. O consumidor deve tolerar a segunda entrega.
- Dois workers: faça dois relays disputarem o mesmo lote e confirme que o claim, o lease e a atualização final não fazem um evento desaparecer.
- Ordem por agregado: grave duas transições do mesmo pedido e verifique a ordem que chega ao consumidor. Teste agregados diferentes separadamente.
- Mensagem venenosa: registre tentativas, erro e payload suficiente para uma triagem. Depois de um limite, mova a mensagem para uma quarentena operável, como a dead-letter queue para tarefas de agentes que falham sempre.
Esse conjunto complementa os testes de idempotência de uma API e os testes de webhooks com duplicatas e retries. Eles exercitam fronteiras diferentes: o outbox protege a obrigação de publicar; o consumidor e a API protegem o efeito de receber ou repetir.
O que o outbox não resolve
O padrão não faz um banco e um broker participarem de uma transação distribuída. Ele também não reverte automaticamente um efeito externo já aceito, não garante que consumidores sejam rápidos e não elimina a necessidade de métricas, retenção e reprocessamento.
Se vários serviços precisam concluir uma transação de negócio, cada serviço ainda terá suas próprias mudanças e falhas. Uma saga pode coordenar essas etapas com compensações, mas isso é uma decisão diferente da publicação confiável de um evento local.
Para um agente, registre também o ID da execução, o ID da ação e a versão do contrato que gerou o evento. Meu RemoteCode é uma ferramenta própria para esse tipo de fluxo. Isso ajuda a ligar uma mudança confirmada ao trabalho que o relay iniciou sem transformar prompts ou segredos em payload de integração.
Use este critério: se perder a notificação depois do commit deixa o sistema em um estado que ninguém consegue reconstruir, persista a obrigação de publicar dentro da mesma transação. Depois trate relay, duplicatas, ordem e recuperação como partes visíveis do sistema.
Fontes
- AWS Prescriptive Guidance, "Transactional outbox pattern", consultado em 2026-10-31.
- microservices.io, "Pattern: Transactional outbox", consultado em 2026-10-31.
- PostgreSQL, "COMMIT", consultado em 2026-10-31.
- PostgreSQL, "SELECT: locking clause", consultado em 2026-10-31.
- Debezium, "Outbox Event Router", consultado em 2026-10-31.
- Microsoft Learn, "Idempotent Consumer pattern", consultado em 2026-10-31.