Desvendando o Outbox Pattern: Garantindo Consistência em Arquiteturas Assíncronas
Em sistemas distribuídos modernos, especialmente aqueles que lidam com processamentos pesados em background ou dependem de integrações via…
Desvendando o Outbox Pattern: Garantindo Consistência em Arquiteturas Assíncronas

Em sistemas distribuídos modernos, especialmente aqueles que lidam com processamentos pesados em background ou dependem de integrações via mensageria, um dos problemas mais clássicos e perigosos é o Problema da Escrita Dupla (Dual Write Problem).
Imagine o cenário: você recebe uma requisição HTTP para atualizar os dados de uma conta. A sua aplicação precisa:
- Salvar o novo estado no banco de dados.
- Publicar um evento em uma fila (como RabbitMQ, Kafka, AWS SQS ou Google Pub/Sub) para notificar outros serviços, ou até mesmo enfileirar um processamento assíncrono interno.
Se você salvar no banco e a fila estiver instável, o evento se perde. Se você publicar na fila primeiro e o commit no banco de dados falhar, o sistema downstream processará uma informação fantasma.
Como garantir que ambas as operações aconteçam de forma segura ou falhem juntas? É aqui que brilha o Transactional Outbox Pattern (ou simplesmente Outbox Pattern).
O que é o Outbox Pattern?
O Outbox Pattern resolve o problema da escrita dupla utilizando transações do banco de dados relacional (como PostgreSQL ou MySQL) a nosso favor.
Em vez de enviar a mensagem para a mensageria ou enfileirar no worker (ex: Sidekiq) no exato momento da requisição, a aplicação grava o estado do negócio e insere um “evento a ser processado” em uma tabela separada (frequentemente chamada de outbox_events), na mesma transação de banco de dados.
Como ambas as inserções fazem parte da mesma transação ACID, ou as duas acontecem, ou nenhuma acontece.
[Cliente] -> [API] -> (Inicia Transação)
|-> Salva no Banco (Tabela Negócio)
|-> Salva na Outbox (Tabela outbox_events)
(Commit da Transação)
Posteriormente, um processo rodando em background (um poller) lê de tempos em tempos essa tabela de outbox_events e se encarrega de publicar a mensagem no serviço de fila de forma confiável, marcando o evento como publicado ou removendo-o da tabela.
Os Dois Lados da Moeda: Benefícios e Trade-offs
Nenhum padrão de arquitetura é uma bala de prata. Antes de adotar o Outbox Pattern, você deve pesar os prós e os contras.
Benefícios
- Consistência Forte: Acaba o risco de inconsistência entre o banco e o Message Broker.
- Resiliência: Se a fila ou o broker caírem, os eventos continuam salvos de forma segura no banco. Quando o broker voltar, os eventos pendentes serão processados sem perda de dados.
- Desacoplamento de Performance: A API responde rapidamente ao cliente (
202 Accepted), deixando o trabalho pesado de rede e retentativas para o background.
Trade-offs (O preço a se pagar)
- Latência: A comunicação deixa de ser em tempo real instantâneo. Há um delay (dependendo de quanto em quanto tempo o poller está rodando) entre o commit no banco e o momento em que o poller envia a mensagem.
- Overhead no Banco de Dados: O seu banco principal (PostgreSQL/MySQL) agora precisa lidar com queries frequentes de leitura e escrita do processo de background.
- Gerenciamento de Expurgos (Purging): Se você não criar uma rotina para limpar ou arquivar os eventos publicados da tabela
outbox_events, ela crescerá infinitamente, degradando a performance do banco ao longo do tempo.
Exemplo Prático: Como aplicamos no motor de pagamentos
No Motor de pagamentos, nós elevamos o uso do Outbox Pattern para orquestrar fluxos complexos, como atualizações e ativações de clientes em provedores externos. Nossa arquitetura dá à tabela de Outbox dois papéis fundamentais, classificados pelo tipo do evento:
- Eventos de Comando (Ex:
workspace_update_requested): Na requisição da API, salvamos a intenção de mudança e o evento na Outbox de forma atômica. - Eventos de Notificação (Ex:
workspace_update_completed): Dentro do worker assíncrono, após processar a integração com o parceiro externo com sucesso, abrimos outra transação: limpamos o comando e geramos um evento de notificação para o ecossistema via Google Pub/Sub.
Olhando para o código, o fluxo da nossa API garante essa atomicidade direto na transação do banco:
ActiveRecord::Base.transaction do
# 1. Salva a intenção de mutação de negócio
mutation_request.save!
# 2. Registra na Outbox na mesma transação
OutboxEvent.create!(
correlation_id: request.request_id,
event_type: :workspace_update_requested,
payload: { mutation_request_id: mutation_request.id }
)
end
Isso garante que nossa API e nossos Jobs commitem com segurança de forma atômica e que notificações só sejam enviadas caso o processamento no provedor seja realmente concluído.
O Lado do Poller e o Perigo Oculto da Concorrência
Para fechar o ciclo do padrão, precisamos de um processo em background para ler esses dados. Um poller básico em Rails buscaria os eventos pendentes e os enviaria para o mundo externo.
Contudo, há um perigo oculto aqui: a concorrência.
Se a sua aplicação rodar em múltiplas instâncias (vários pods no Kubernetes, por exemplo), duas instâncias do seu poller podem fazer a query de busca exatamente no mesmo milissegundo. Elas lerão os mesmos eventos e dispararão mensagens duplicadas para a fila.
Para resolver isso de forma elegante no PostgreSQL e Rails, utilizamos concorrência pessimista com a cláusula SKIP LOCKED. Ela diz ao banco para trancar as linhas que uma instância está lendo e instrui as outras instâncias a pularem essas linhas trancadas.
# app/workers/outbox_poller_worker.rb
class OutboxPollerWorker
def perform
# O uso do lock garante isolamento seguro entre múltiplas instâncias do poller
events = OutboxEvent.where(published: false)
.lock("FOR UPDATE SKIP LOCKED")
.limit(100)
events.each do |event|
# Publica no Message Broker (ex: Google Pub/Sub)
MessagingClient.publish(topic: event.event_type, payload: event.payload)
# Marca como processado
event.update!(published: true)
end
end
end
Boas Práticas Cruciais
- Idempotência é obrigatória: O Outbox Pattern garante a entrega pelo menos uma vez (At-Least-Once). Se o processo falhar logo após enviar a mensagem para a fila, mas antes de atualizar o banco para
published: true, o evento será enviado novamente. O sistema que consome a mensagem precisa saber lidar com duplicidades. - Mantenha os payloads enxutos: Evite salvar grandes estruturas de dados na tabela de Outbox. Guarde apenas identificadores (
workspace_id,user_id) e o tipo do evento. Deixe que o processo que consome a mensagem busque os dados atualizados se necessário.
Alternativa Moderna no Rails: Solid Queue
Uma provocação riquíssima para quem desenvolve no ecossistema Ruby on Rails: se o seu objetivo de usar o Outbox Pattern com Sidekiq é garantir que o enfileiramento de um job interno seja atômico com o banco, o ecossistema moderno trouxe uma evolução fantástica.
Antigamente, usar o Sidekiq (Redis) criava o Dual Write Problem porque o Redis está fora da transação do banco relacional. Contudo, adapters modernos baseados no próprio banco de dados, como o Solid Queue (padrão oficial do Rails 8) ou o Good Job, eliminam essa dor para fluxos internos.
Como as tabelas de filas do Solid Queue residem no mesmo banco de dados relacional da aplicação, quando você dispara um job dentro de um bloco de transação do ActiveRecord, o enfileiramento torna-se nativamente atômico:
ActiveRecord::Base.transaction do
mutation_request.save!
# Usando Solid Queue, isso se torna nativamente uma única transação ACID.
# Se o commit falhar, o job nunca vai para a fila!
WorkspaceUpdateJob.perform_later(mutation_request.id)
end
Para integrações e chamadas de workers puramente internas, os adapters de banco eliminam a necessidade de construir uma arquitetura de Outbox manual. Deixamos o Outbox Pattern clássico para quando precisamos de fatos de negócio estritos ou comunicação direta com brokers externos (como o Pub/Sub).
Agradecimentos
Construir arquiteturas escaláveis, resilientes e seguras no mundo financeiro exige muito cuidado.
Esse belíssimo fluxo desenhado não seria possível sem a excelência e o rigor técnico de todo o time de engenharia do Motor de Pagamentos. Um agradecimento especial a todo o time pelas discussões arquiteturais ricas, revisão de PRs minuciosas e pelo empenho contínuo em adotar os melhores padrões do mercado em nosso ecossistema.
A cultura de qualidade reflete diretamente na nossa entrega. Seguimos juntos construindo o futuro dos pagamentos!
메타데이터
- post_id
- 4da59b4f3e22
- slug
- desvendando-o-outbox-pattern-garantindo-consistência-em-arquiteturas-assíncronas-4da59b4f3e22
- url
- https://medium.com/totvsdevelopers/desvendando-o-outbox-pattern-garantindo-consist%C3%AAncia-em-arquiteturas-ass%C3%ADncronas-4da59b4f3e22
- canonical_url
- https://medium.com/totvsdevelopers/desvendando-o-outbox-pattern-garantindo-consist%C3%AAncia-em-arquiteturas-ass%C3%ADncronas-4da59b4f3e22
- author_url
- https://medium.com/@renan-porto1099
- status
- ok
- fetched_at
- 2026-06-17 17:19:58