rubyguides

Observer pattern in Ruby: Observable, custom observers, and events

The observer pattern is a design pattern where an object (the subject) maintains a list of dependent objects (observers) and notifies them automatically of any state changes. It is one of the classic Gang of Four patterns and shows up constantly in Ruby code, often without you noticing.

This guide shows how the pattern works in Ruby, how the standard library implements it, how to roll your own version, and when an event system like ActiveSupport::Notifications is the better fit.

The pattern is useful when one change should trigger several follow-up actions. Instead of making a single class know about every side effect, you let observers react on their own. That keeps the subject smaller and makes the surrounding behavior easier to extend. A newsletter that publishes an article might trigger email delivery, analytics tracking, search re-indexing, and cache invalidation, all without the newsletter class knowing those details exist. Instead of making a single class know about every side effect, you let observers react on their own. That keeps the subject smaller and makes the surrounding behavior easier to extend.

what problem does it solve?

Imagine you have a Newsletter class that sends emails when new articles are published. Later you add analytics tracking. Then you add a search indexer. Each new feature adds more responsibility to the same class, and every caller site needs to know about all the downstream effects.

The observer pattern decouples this. The Newsletter only knows it has observers. It broadcasts events. It has no idea what those observers do.

That separation becomes even more useful when the list of side effects changes over time. New observers can be added without rewriting the subject, and the original object stays focused on its own job.

# Without the observer pattern, Newsletter knows too much
class Newsletter
  def initialize
    @subscribers = []
  end

  def subscribe(email)
    @subscribers << email
  end

  def publish(article)
    # This class knows about every side effect
    @subscribers.each { |email| send_email(email, article) }
    Analytics.track_article_view(article)
    SearchIndex.reindex(article)
    # More integrations keep piling up...
  end
end

The observer pattern breaks this dependency chain.

In practice, that means you can add analytics, notifications, indexing, or logging without turning the subject into a kitchen sink. Each observer can stay small and handle one reaction cleanly.

ruby’s built-in observable module

Ruby’s standard library ships with Observable out of the box. You get it by requiring 'observer' and mixing it into your subject class.

require 'observer'

class Article
  include Observable

  attr_reader :title, :body

  def initialize(title, body)
    @title = title
    @body = body
  end

  def publish
    changed # marks the subject as having changed
    notify_observers(self) # passes self to all observers' update method
  end
end

notify_observers calls the update method on every registered observer, passing the subject as an argument.

That default shape is easy to remember: the subject changes, then every observer gets a chance to react. The subject does not need to know which observer does what, only that each one understands the update message.

Define observers by implementing an update(subject) method:

The observers themselves stay very small. They only need to know how to respond to the event, not how the event was produced or how many other observers are listening.

class EmailSubscriber
  def update(article)
    puts "Sending email about: #{article.title}"
  end
end

class SearchIndexer
  def update(article)
    puts "Indexing article: #{article.title}"
  end
end

When you call add_observer, the subject stores a reference to each observer in an internal list. The order of registration matters because notify_observers iterates through the list sequentially. You can register any object that responds to update, which means observers can be plain classes, service objects, or even lambdas wrapped in a small adapter. Here is how that looks in practice:

article = Article.new("Observer Pattern Guide", "Here's how it works...")
article.add_observer(EmailSubscriber.new)
article.add_observer(SearchIndexer.new)

article.publish
# => Sending email about: Observer Pattern Guide
# => Indexing article: Observer Pattern Guide

Once the observers are registered, the subject can publish and move on. That keeps the subject code short, and it lets the observers evolve independently as the application grows. The same pattern works with any number of observers: adding a third or fourth listener does not change the subject’s code at all, which is the main reason this design scales well as features accumulate.

controlling when to notify

changed has a truthy guard: it only marks the object as changed if you pass it a truthy value.

changed(true)  # marks as changed
changed(false) # marks as unchanged
changed        # equivalent to changed(true)

The changed method acts like a flag that resets after each notification cycle. Once notify_observers runs, the flag clears automatically, so the same subject can change again later and trigger a fresh round of notifications. This prevents observers from being called repeatedly when nothing has actually changed.

notify_observers only fires if changed? returns true:

article = Article.new("Draft Post", "Still editing...")
article.add_observer(EmailSubscriber.new)

article.publish # No output; nothing changed since last notification

article.changed(true)
article.publish # Observer fires

This guard mechanism is useful when the subject’s internal state changes in steps and you only want to notify observers after the final step. For example, you might update several fields on a record and call changed only after the last assignment, so observers see the fully resolved state rather than a series of intermediate snapshots.

notify_observers also accepts an optional argument passed directly to observers, bypassing the subject:

article.notify_observers("custom message") # observers receive "custom message" instead of article

That flexibility is handy when the event payload matters more than the object that produced it. You can send a lightweight value when the subject itself is not the best representation of the change.

building a custom implementation

Ruby’s Observable is convenient, but understanding the mechanics makes you a better developer, and sometimes you need more control.

A minimal custom observer system needs just a few pieces:

module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def remove_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers(event)
    @observers.each { |observer| observer.update(event) }
  end
end

The custom version makes the moving parts visible. You can see how observers are stored, how they are removed, and how the notification loop reaches each subscriber in turn.

Apply it by extending or including the module:

The subject stays responsible for state changes, while observers keep their own behavior. That split makes the code easier to extend because each side has one clear job.

class Order
  include Subject

  attr_reader :status

  def initialize
    super()
    @status = :pending
  end

  def complete
    @status = :completed
    notify_observers(self)
  end
end

Observers just need an update method:

Because the interface is so small, the observer objects can stay focused. One observer can log, another can update billing, and a third can refresh a cache without any of them needing to know about the others. The subject only calls update and passes itself as the argument; everything else is the observer’s responsibility. That single-method contract means you can swap in test doubles or add new behaviours without touching the subject at all.

class OrderLogger
  def update(order)
    puts "Order #{order.object_id} is now #{order.status}"
  end
end

class AccountingSystem
  def update(order)
    return unless order.status == :completed
    puts "Billing customer for completed order"
  end
end

Each observer handles a single concern. OrderLogger writes a line to the log regardless of the new status, while AccountingSystem only reacts when the order reaches the completed state. That conditional check inside the observer keeps the subject (Order) free of business rules that belong elsewhere. The wiring code below shows how both observers attach to the same order instance without the order needing to know which specific actions will fire.

order = Order.new
order.add_observer(OrderLogger.new)
order.add_observer(AccountingSystem.new)

order.complete
# => Order 1234567890 is now completed
# => Billing customer for completed order

a more rubyesque approach with blocks

You can also build an observer system using Procs and lambdas, which is a common pattern in Ruby. Instead of maintaining an explicit list of observer objects, you store callable blocks keyed by event name. This approach feels more idiomatic in Ruby because blocks and Procs are first-class citizens that you can pass around as naturally as any other value.

class EventEmitter
  def initialize
    @listeners = Hash.new { |h, k| h[k] = [] }
  end

  def on(event_name, &block)
    @listeners[event_name] << block
  end

  def emit(event_name, *args)
    @listeners[event_name].each { |block| block.call(*args) }
  end

  def off(event_name, block)
    @listeners[event_name].delete(block)
  end
end

The EventEmitter class generalises the observer idea by letting callers register for named events instead of a single update method. Under the hood it still walks a list of callbacks, but the naming layer makes it easier to reason about when several different event types flow through the same object. You can register multiple listeners for the same event, and each one runs in the order it was added.

emitter = EventEmitter.new

logger = ->(msg) { puts "LOG: #{msg}" }
emitter.on(:message, logger)
emitter.on(:message, ->(msg) { puts "ALERT: #{msg}" })

emitter.emit(:message, "Something happened")
# => LOG: Something happened
# => ALERT: Something happened

emitter.off(:message, logger)
emitter.emit(:message, "Only alert fires now")
# => ALERT: Only alert fires now

This pattern is the foundation of many Ruby event systems and is essentially the observer pattern with a more dynamic registration model. The ability to remove individual listeners gives you fine-grained control that the standard library Observable module does not offer directly. You can also attach different blocks for different event names, which keeps the callback logic organised by topic rather than by object type.

Blocks can be a better fit when the callback is small and local to the place where the event is registered. They keep the code close to the event source, while still giving you a clean way to fan out work.

practical examples

model observers (plain ruby)

A common use case is separating business logic from domain models. Rather than stuffing notification code into your model, you attach observers:

Keeping the model narrow makes the side effects easier to swap out later. The model can publish the change, and each observer can decide what to do with it.

That style works especially well when one domain change should trigger several unrelated tasks, because the model stays focused on the domain update while the observers handle email, auditing, and cache invalidation in separate places.

Below is a concrete example. The User model knows only that it has observers. Email delivery and audit logging live in separate classes, each with a single responsibility. That separation means you can add a third observer (say, a cache invalidator) without touching the model at all.

class User
  include Observable

  attr_accessor :email, :name

  def initialize(email, name)
    @email = email
    @name = name
  end

  def update_profile(name)
    @name = name
    changed(true)
    notify_observers(self)
  end
end

class WelcomeMailer
  def update(user)
    puts "Sending welcome email to #{user.email}"
  end
end

class ProfileAuditor
  def update(user)
    puts "Audit log: #{user.name} updated their profile"
  end
end

user = User.new("alice@example.com", "Alice")
user.add_observer(WelcomeMailer.new)
user.add_observer(ProfileAuditor.new)

user.update_profile("Alice Smith")
# => Audit log: Alice Smith updated their profile

multi-event notification systems

A richer pattern supports named events rather than a single “something changed” notification. The User example above uses a single notify_observers call that fires every registered observer, which works well when the subject has only one kind of event to broadcast. In larger systems, you often want to differentiate between event types so that observers can subscribe to exactly the events they care about without filtering inside their update method.

class Blog
  def initialize
    @observers = Hash.new { |h, k| h[k] = [] }
  end

  def on(event, &callback)
    @observers[event] << callback
  end

  def emit(event, *args)
    @observers[event].each { |cb| cb.call(*args) }
  end
end

The Blog class demonstrates multi-event notification without the standard library. Each call to on registers a callback under a named event, and emit fires every callback registered for that event name. This design is especially useful when the same object needs to signal different kinds of changes: a post being published, a comment being added, or a tag being updated—without forcing every listener to inspect the payload to figure out what happened.

blog = Blog.new

blog.on(:post_published) do |article, author|
  puts "#{author} published: #{article}"
end

blog.on(:comment_added) do |article, commenter|
  puts "#{commenter} commented on: #{article}"
end

blog.emit(:post_published, "Observer Pattern", "Alice")
# => Alice published: Observer Pattern

Named events give you the freedom to fire only the relevant callbacks. When a comment is added, the post-published listeners do not run, and vice versa. This selective dispatch keeps each concern isolated and makes the system easier to extend one event type at a time without worrying about side effects on unrelated listeners.

observer pattern vs signals/slots

Qt uses a signals and slots mechanism that’s conceptually similar to the observer pattern but with some important differences:

AspectObserver PatternSignals/Slots
CouplingSubject and observer loosely coupledVery loose; no interface required
Type safetyCompile-time checking possibleUsually runtime-based
FlexibilityFixed update signatureNamed signals, varied slot signatures
Use in RubyStandard Ruby idiomsQt Ruby bindings only

In pure Ruby, you don’t have signals and slots, you have the observer pattern and its variants. The EventEmitter style shown above is the closest native-Ruby equivalent to signals/slots.

If you’re working in a Qt/Ruby application, prefer signals and slots. For everything else, the observer pattern is your tool.

That recommendation is really about environment, not preference. Use the native tool for the framework you are already in, and use the plain Ruby pattern when you want a lightweight event mechanism inside your own code.

ActiveSupport::Notifications

If you’re in a Rails environment, ActiveSupport::Notifications is the idiomatic way to implement observer-like pub/sub:

This is the most practical option when the application already lives inside Rails. It gives you a standard event bus, and the framework itself is already set up to use it in places like controller instrumentation and action tracking.

# Instrumenter publishes events
ActiveSupport::Notifications.instrument("article.published", article: @article) do
  @article.save!
end

# Subscriber listens for events
ActiveSupport::Notifications.subscribe("article.published") do |event|
  article = event.payload[:article]
  puts "Article was published: #{article.title}"
end

benefits over the standard observer pattern

  • Asynchronous delivery. Events can be processed in a background thread.
  • Multiple subscribers. Unlimited observers per event.
  • Event history. ActiveSupport::Notifications can buffer and fan-out events.
  • Namespacing. Dot-notation events like action_controller.process_action organize naturally.

when to use each

ScenarioRecommendation
Simple in-process notificationRuby Observable or custom observer
Multiple named eventsCustom EventEmitter-style class
Rails applicationActiveSupport::Notifications
Cross-process / service messagingMessage queues (SQS, RabbitMQ)

thread safety considerations

Ruby’s Observable is not thread-safe by default. If you’re working in a multi-threaded environment (e.g., a web server like Puma), you need to synchronize access:

The problem is not the observer idea itself, it is the shared mutable state around the observer list. Once multiple threads can add or notify at the same time, the implementation needs a lock so one thread does not read a half-updated list.

require 'observer'
require 'thread'

class ThreadSafeArticle
  include Observable
  attr_reader :title

  def initialize(title)
    super()
    @title = title
    @lock = Mutex.new
  end

  def add_observer(observer)
    @lock.synchronize do
      super(observer)
    end
  end

  def remove_observer(observer)
    @lock.synchronize do
      super(observer)
    end
  end

  def notify_observers(arg = nil)
    @lock.synchronize do
      super(arg)
    end
  end

  def publish
    changed
    notify_observers(self)
  end
end

For Rails applications, ActiveSupport::Notifications handles thread safety internally. For custom implementations, always wrap observer registration and notification in mutex locks:

That keeps the critical section small and makes the behavior easier to reason about. If the event is going to be processed later, use the lock to capture the state you need and then let the observer run with that snapshot.

@mutex = Mutex.new

def safe_notify(event)
  @mutex.synchronize { notify_observers(event) }
end

When using Queue to process observer notifications asynchronously, be aware that the subject may change state between when the event is queued and when it’s processed. Pass a snapshot of relevant data rather than a reference to the subject itself:

This is especially important in web applications where the original object may be reloaded, mutated, or even destroyed before the background consumer runs. A small snapshot avoids that timing problem and keeps the payload stable.

# Prefer an immutable snapshot
notify_observers({ title: @title, status: @status, timestamp: Time.now })

# Avoid using the subject directly, because its state may have changed by the time the observer runs
notify_observers(self)

These thread-safety rules are not unique to the observer pattern. They apply anywhere you queue work for later, but the observer pattern makes them very visible because notification is the whole point of the design.

The observer pattern works best when you keep the event small, the observers focused, and the shared state protected. That balance gives you the flexibility of pub/sub without making the code hard to follow.

See Also