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

Outboxer Wiki – Table of Contents

Foundation

  1. Define Ubiquitous Language
    Model your core business actions clearly, per bounded context.

  2. Decoupling with Events
    How to migrate to an event-driven architecture.

  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.


Define Ubiquitous Language

Start by identifying domain concepts in your bounded context. For example, in the Accountify context, voiding an invoice is a meaningful domain action. We want to:

  • Capture this behavior explicitly
  • Represent it in code using a clear name
  • Allow other systems to react to it without tight coupling

This leads us to define a domain event: InvoiceVoidedEvent.


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 InvoiceVoidedEvent < 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::InvoiceVoidedEvent"
      Notifier.send_invoice_voided_email(event.body["invoice_id"])
      Analytics.track_invoice_voided(event.tenant_id, event.body)

    # Add other event types here
    else
      Rails.logger.warn("Unhandled event type: #{event.type}")
    end
  end
end

Step 3: Use 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 void(user_id:, tenant_id:, id:)
      ActiveRecord::Base.transaction do
        invoice = Invoice.find(id)
        invoice.update!(status: 'voided')

        event = InvoiceVoidedEvent.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: 'voided')

  event = InvoiceVoidedEvent.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 = InvoiceVoidedEvent.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 InvoiceVoidedEvent < 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 void(user_id:, tenant_id:, id:)
      ActiveRecord::Base.transaction do
        invoice = Invoice.find(id)
        invoice.update!(status: 'voided')

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