ruby 56 lines · 3 tabs

Instrument a Service with Notifications

Shared by codesnips Jan 2026
3 tabs
class CheckoutService
  def initialize(cart:, user:)
    @cart = cart
    @user = user
  end

  def call
    payload = { user_id: @user.id, cart_id: @cart.id, item_count: @cart.items.size }

    ActiveSupport::Notifications.instrument("checkout.commerce", payload) do
      order = Order.create!(user: @user, total_cents: @cart.total_cents)
      @cart.items.each { |item| order.line_items.create!(from: item) }
      PaymentGateway.charge!(order)

      payload[:order_id] = order.id
      payload[:total_cents] = order.total_cents
      order
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows how to add observability to a plain Ruby service object using ActiveSupport::Notifications, the same pub/sub bus Rails uses internally for events like sql.active_record and process_action.action_controller. The core idea is decoupling: the service emits named events with a payload, and any number of subscribers react to them without the service knowing who is listening. This keeps timing, metrics, and logging concerns out of the business logic.

In CheckoutService, the work is wrapped in ActiveSupport::Notifications.instrument. That call yields to the block, records a monotonic start and finish time, and publishes a single checkout.commerce event carrying a payload hash. The block mutates payload after the work runs so subscribers can see the resulting order_id and item count — a common trick, since the same hash reference passed to instrument is delivered to subscribers. If the block raises, instrument still fires the event but adds an :exception and :exception_object entry to the payload, so failures are observable too.

The naming convention matters: events are named action.component (dot-separated, most-specific-first-ish), because subscribers can match either an exact string or a regex. checkout.commerce fits the pattern Rails expects.

In CheckoutMetricsSubscriber, ActiveSupport::Notifications.subscribe receives an ActiveSupport::Notifications::Event when passed a block with a single argument, which conveniently exposes event.duration in milliseconds and the event.payload. Here it forwards a timing metric and an error counter to a StatsD-style client, branching on whether payload[:exception] is present. Registering the subscriber once at boot (in an initializer) is important — subscribing repeatedly leaks listeners.

In checkout_instrumentation.rb, the initializer wires everything together and also attaches a lightweight tagged logger subscription inline, demonstrating that multiple independent subscribers can observe the same event. The main trade-off of this pattern is indirection: control flow is harder to trace because emitters and handlers are separate, and a slow synchronous subscriber will slow the instrumented call. It is the right tool when cross-cutting concerns should stay orthogonal to domain code.


Related snips

Share this code

Here's the card — post it anywhere.

Instrument a Service with Notifications — share card
Link copied