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

Outboxer

Table of Contents


Decoupling with Events

The Problem

In many Rails apps, models are responsible for triggering everything:

  • Sending emails
  • Enqueuing jobs
  • Syncing with external systems
  • Updating dashboards

This leads to tight coupling—where core business logic is tangled with infrastructure code. Over time, changes become riskier, tests more brittle, and debugging harder.

The Solution: Create Events

Instead of directly triggering everything, a model just queues a message describing what happened. Other parts of the system can react later—without knowing or caring about the source.

module Accountify
  class Invoice < ApplicationRecord
    def finalise!
      update!(status: 'finalised')

      Outboxer::Message.queue(
        messageable: self,
        type: 'Accountify::Invoice::Finalised'
      )
    end
  end
end

The model stays focused. The message acts as a signal that something meaningful occurred.

Why this matters

  • Aggregates remain clean and focused
  • Behavior can be extended without modifying core logic
  • System is easier to test, understand, and evolve

💾 The Transactional Outbox Pattern

The Problem: Lost or Inconsistent Events

If you update a record and publish an event in separate steps, it's easy to run into subtle, high-risk bugs:

  • The record saves, but the event fails to publish
  • The event publishes, but the database transaction rolls back

This leaves the system in an inconsistent state.

The Solution: Queue Messages Transactionally

Outboxer uses the transactional outbox pattern to ensure event reliability. You use Outboxer::Message.queue inside the same transaction as your business logic:

ActiveRecord::Base.transaction do
  contact = Contact.create!(...)

  Outboxer::Message.queue(
    messageable: contact,
    type: 'Accountify::Contact::Created'
  )
end

The message is only saved if the transaction commits successfully.

Publishing the Messages

Messages are later published by a background job or loop:

Outboxer::Publisher.publish_messages do |_, messages|
  messages.each do |message|
    Sidekiq.perform_async("HandleMessageJob", message.payload)
  end
end

Why this works

  • Messages are only published after successful commits
  • There’s no need to coordinate message delivery manually
  • The system remains consistent, even during crashes or retries

Clone this wiki locally