Guide

Transactional outbox

Publish inside a transaction — the event commits with your data, or rolls back with it.

The dual-write problem#

Write a row and publish an event and you have two systems to keep in sync. If the publish happens before commit, a rollback leaves a phantom event for data that never landed. If it happens after commit, a crash in between loses the event. The outbox closes that gap: the event is written to an outbox table in the same transaction as your data, and a poller publishes it afterward.

The outbox row commits with the business row — or rolls back with it. A poller moves only committed rows into PGMQ.

Enable it#

rails generate pgbus:add_outbox                  # add the outbox migration
rails generate pgbus:add_outbox --database=pgbus # for a separate database
rails db:migrate
config/initializers/pgbus.rb
Pgbus.configure do |config|
  config.outbox_enabled       = true
  config.outbox_poll_interval = 1.0   # seconds
  config.outbox_batch_size    = 100
  config.outbox_retention     = 1.day # Duration also accepted
end

Publish inside a transaction#

Call Pgbus::Outbox.publish (a queue) or publish_event (a topic) inside the transaction that writes your data. Both the row and the outbox entry commit together — or roll back together:

ActiveRecord::Base.transaction do
  order = Order.create!(params)

  # Committed atomically with the order. If the transaction rolls back,
  # so does the outbox entry — no phantom event.
  Pgbus::Outbox.publish("default", { order_id: order.id })

  # For the topic-based event bus:
  Pgbus::Outbox.publish_event("orders.created", { order_id: order.id })
end

How the poller works#

The outbox poller is a supervised process. Each cycle it claims a batch of unpublished entries with FOR UPDATE SKIP LOCKED (so multiple pollers never double-publish), sends them to PGMQ, and marks them published. An entry that fails to publish is simply skipped and retried next cycle. Published entries are purged after outbox_retention.

The outbox pairs naturally with idempotent event handlers: at-least-once delivery plus idempotent! means a re-published entry is handled exactly once. See Event bus.