ruby 74 lines · 4 tabs

In-Process Domain Event Bus with Subscribers in Rails

Shared by codesnips Aug 2026
4 tabs
module EventBus
  @subscribers = Hash.new { |h, k| h[k] = [] }

  class << self
    def subscribe(event_class, handler = nil, &block)
      callable = handler || block
      raise ArgumentError, "handler must respond to #call" unless callable.respond_to?(:call)
      @subscribers[event_class] << callable
    end

    def publish(event)
      @subscribers[event.class].each do |handler|
        begin
          handler.call(event)
        rescue => e
          Rails.error.report(e, handled: true, context: { event: event.class.name })
        end
      end
      event
    end

    def reset!
      @subscribers = Hash.new { |h, k| h[k] = [] }
    end
  end
end
4 files · ruby Explain with highlit

This snippet shows a small in-process domain event bus that lets a Rails application publish domain events and dispatch them to interested handlers without coupling the code that raises the event to the code that reacts to it. The pattern is the classic publish/subscribe (observer) idea scoped to a single process: business logic announces that something happened, and any number of subscribers respond independently.

In EventBus, the bus is a singleton-style module holding a registry keyed by event class. subscribe appends a callable handler for a given event type, and publish looks up every handler registered for that event's exact class and invokes it. Handlers are stored as anything responding to call, so lambdas, method objects, or service classes all fit. Each handler runs inside a rescue so one failing subscriber never prevents the others from running; failures are reported to Rails.error rather than raised, which keeps event dispatch best-effort by design.

In OrderPlaced event, the event itself is a plain immutable value object built with Struct. It carries only the data a subscriber needs — order_id, total_cents, and occurred_at — and defaults the timestamp at construction. Keeping events as dumb data means subscribers never reach back into mutable application state and the event can be logged or serialized as-is.

In OrdersController, the controller performs the write inside a transaction and calls EventBus.publish only after the record is durably committed via after_commit-style ordering, avoiding the common pitfall of firing events for a transaction that later rolls back. The controller stays thin: it knows nothing about email, analytics, or inventory.

In subscribers initializer, the wiring lives in one boot-time file so the set of reactions is easy to audit. Each subscription maps OrderPlaced to a concrete responsibility — enqueuing a confirmation mailer job and tracking an analytics event — and the handlers push slow work onto background jobs rather than doing it inline.

The main trade-off is that this bus is synchronous and single-process: it is simple and transactional-friendly but does not survive restarts or cross service boundaries. It is ideal for decoupling modules inside one Rails app; for cross-service or durable delivery, an outbox plus a real broker is the next step.


Related snips

Share this code

Here's the card — post it anywhere.

In-Process Domain Event Bus with Subscribers in Rails — share card
Link copied