-
Notifications
You must be signed in to change notification settings - Fork 1
Home
-
Introduction
Why Outboxer exists. Event-driven architecture + DDD + the outbox pattern. -
Decoupling with Events
HowOutboxer::Message.queuehelps aggregates emit events without tight coupling. -
The Transactional Outbox Pattern
How to avoid message loss and maintain consistency. -
Use a Callback to DRY Up Message Queuing
Use a baseEventclass with anafter_createhook to reduce repetition and ensure consistency.
Outboxer is a battle-tested implementation of the transactional outbox pattern for Ruby on Rails applications.
It helps you build reliable event-driven systems grounded in Domain-Driven Design (DDD) principles—without the risk of lost messages, tight coupling, or inconsistent state.
Outboxer works by capturing events inside the same transaction as your business logic, and publishing them safely after commit using Outboxer::Message.queue.
Ever seen a model that does everything? From saving records to sending emails and syncing with external services?
That’s a recipe for trouble. When logic, integrations, and infrastructure mix in the same place, bugs become harder to track and tests become fragile. One change can break ten things.
Start with a base event class to standardize how you track meaningful business actions:
class Event < ApplicationRecord
self.abstract_class = true
endNow define a domain event that inherits from it:
module Accountify
class InvoiceFinalisedEvent < Event
belongs_to :eventable, polymorphic: true
end
endclass EventCreatedJob
include Sidekiq::Job
def perform(event_id)
event = Event.find(event_id)
case event.type
when "Accountify::InvoiceFinalisedEvent"
Notifier.send_invoice_finalised_email(event.body["invoice_id"])
Analytics.track_invoice_finalised(event.tenant_id, event.body)
# Add other event types here
else
Rails.logger.warn("Unhandled event type: #{event.type}")
end
end
endTo decouple side effects, begin by creating an event and queuing a background job in the application service.
module Accountify
module InvoiceService
extend self
def finalise(user_id:, tenant_id:, id:)
ActiveRecord::Base.transaction do
invoice = Invoice.find(id)
invoice.update!(status: 'finalised')
event = InvoiceFinalisedEvent.create!(
user_id: user_id,
tenant_id: tenant_id,
eventable: invoice,
body: { invoice_id: invoice.id }
)
EventCreatedJob.perform_async(event.id)
end
end
end
endIf you update a record and publish a message in two separate steps, you risk:
- Saving the record but losing the message
- Sending the message but rolling back the record
This leads to inconsistent state between systems.
Outboxer solves this using the transactional outbox pattern. Just queue the message in the same transaction:
ActiveRecord::Base.transaction do
invoice.update!(status: 'finalised')
event = InvoiceFinalisedEvent.create!(
user_id: user_id,
tenant_id: tenant_id,
eventable: invoice,
body: { invoice_id: invoice.id }
)
Outboxer::Message.queue(messageable: event)
endOutboxer will only publish the message if the transaction commits successfully.
Run a loop or background worker to publish messages:
Outboxer::Publisher.publish_messages do |_, messages|
messages.each do |message|
EventCreatedJob.perform_async(message.messageable_id)
end
endYou can use Sidekiq, Delayed Job, or any background job system.
Every time you create an event, you also need to remember to queue it:
event = InvoiceFinalisedEvent.create!(...)
Outboxer::Message.queue(messageable: event)This is easy to forget and clutters your code.
Define an after_save callback that automatically queues outboxer messages:
class Event < ApplicationRecord
# ...
after_create do
Outboxer::Message.queue(messageable: self)
end
endNow your event classes inherit from this:
module Accountify
class InvoiceFinalisedEvent < Event
belongs_to :eventable, polymorphic: true
end
endYour service now just creates the event:
module Accountify
module InvoiceService
extend self
def finalise(user_id:, tenant_id:, id:)
ActiveRecord::Base.transaction do
invoice = Invoice.find(id)
invoice.update!(status: 'finalised')
InvoiceFinalisedEvent.create!(
user_id: user_id,
tenant_id: tenant_id,
eventable: invoice,
body: { invoice_id: invoice.id }
)
end
end
end
endNo need to call Outboxer::Message.queue—it happens automatically.
- ✅ Ensures every event is queued safely
- ✅ Reduces boilerplate
- ✅ Keeps services focused on business logic
- ✅ Still uses the transactional outbox pattern under the hood