The Observer Pattern in Ruby

· 7 min read · Updated March 31, 2026 · intermediate
ruby observer-pattern design-patterns callbacks

The Observer Pattern in Ruby

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’s one of the classic Gang of Four patterns and shows up constantly in Ruby code — often without you noticing.

In this guide you’ll learn how the pattern works, how Ruby’s standard library implements it, how to roll your own, and when to reach for alternatives like ActiveSupport::Notifications.

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.

# 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.

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.

Define observers by implementing an update(subject) method:

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

Wire them up with add_observer:

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

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)

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

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

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

Apply it by extending or including the module:

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:

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
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:

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
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.

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:

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:

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
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

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.

ActiveSupport::Notifications

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

# 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 historyActiveSupport::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:

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:

@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:

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

# Avoid — subject state may have changed by the time observer processes it
notify_observers(self)

See Also