Skip to content
Adam Mikulasev edited this page Jun 14, 2025 · 10 revisions

📃 Outboxer Wiki – Table of Contents

🔀 Foundation

  1. Introduction
    Why Outboxer exists. Event-driven architecture + DDD + the outbox pattern.

  2. Decoupling with Events
    How Outboxer::Message.queue helps aggregates emit events without tight coupling.

  3. The Transactional Outbox Pattern
    How to avoid message loss and maintain consistency.

  4. Use a Callback to DRY Up Message Queuing
    Use a base Event class with an after_create hook to reduce repetition and ensure consistency.


📘 Introduction

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.


🔄 Decoupling with Events

The Problem

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.


Step 1: Create a Root Event Class

Start with a base event class to standardize how you track meaningful business actions:

class Event < ApplicationRecord
  self.abstract_class = true
end

Now define a domain event that inherits from it:

module Accountify
  class InvoiceFinalisedEvent < Event
    belongs_to :eventable, polymorphic: true
  end
end

Step 2: Define an Event Handler Job

class 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
end

Step 3: Start with a Stateless Application Service

To 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
end

💾 The Transactional Outbox Pattern

The Problem: Lost or Inconsistent Events

If 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.


The Solution: Queue Messages in the Same Transaction

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)
end

Outboxer will only publish the message if the transaction commits successfully.


Background Publisher

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
end

You can use Sidekiq, Delayed Job, or any background job system.


🪝 Use a Callback to DRY Up Message Queuing

The Problem: Repeating Yourself

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.


The Solution: Use a callback in the base event class

Define an after_save callback that automatically queues outboxer messages:

class Event < ApplicationRecord
  # ...

  after_create do
    Outboxer::Message.queue(messageable: self)
  end
end

Now your event classes inherit from this:

module Accountify
  class InvoiceFinalisedEvent < Event
    belongs_to :eventable, polymorphic: true
  end
end

Application Services Stay Clean

Your 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
end

No need to call Outboxer::Message.queue—it happens automatically.


Why This Works

  • ✅ Ensures every event is queued safely
  • ✅ Reduces boilerplate
  • ✅ Keeps services focused on business logic
  • ✅ Still uses the transactional outbox pattern under the hood

Clone this wiki locally