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
class CheckoutMetricsSubscriber
def self.attach
ActiveSupport::Notifications.subscribe("checkout.commerce") do |event|
tags = ["user:#{event.payload[:user_id]}"]
if event.payload[:exception]
StatsD.increment("checkout.failed", tags: tags)
else
StatsD.increment("checkout.succeeded", tags: tags)
StatsD.gauge("checkout.item_count", event.payload[:item_count], tags: tags)
end
StatsD.timing("checkout.duration", event.duration, tags: tags)
end
end
end
Rails.application.config.after_initialize do
CheckoutMetricsSubscriber.attach
ActiveSupport::Notifications.subscribe("checkout.commerce") do |event|
if (error = event.payload[:exception])
Rails.logger.error(
"[checkout] failed for user=#{event.payload[:user_id]} " \
"in #{event.duration.round(1)}ms: #{error.join(': ')}"
)
else
Rails.logger.tagged("checkout") do
Rails.logger.info(
"order=#{event.payload[:order_id]} " \
"items=#{event.payload[:item_count]} " \
"took=#{event.duration.round(1)}ms"
)
end
end
end
end
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
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.