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:
| Aspect | Observer Pattern | Signals/Slots |
|---|---|---|
| Coupling | Subject and observer loosely coupled | Very loose; no interface required |
| Type safety | Compile-time checking possible | Usually runtime-based |
| Flexibility | Fixed update signature | Named signals, varied slot signatures |
| Use in Ruby | Standard Ruby idioms | Qt 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::Notificationscan buffer and fan-out events. - Namespacing. Dot-notation events like
action_controller.process_actionorganize naturally.
when to use each
| Scenario | Recommendation |
|---|---|
| Simple in-process notification | Ruby Observable or custom observer |
| Multiple named events | Custom EventEmitter-style class |
| Rails application | ActiveSupport::Notifications |
| Cross-process / service messaging | Message 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
- /guides/ruby-blocks-procs-lambdas/ — Understanding Ruby’s callable objects is foundational to implementing flexible observer callbacks
- /guides/ruby-concurrency-threads/. Thread safety considerations for concurrent observer systems
- /guides/ruby-decorators-pattern/. Another structural pattern for extending object behavior without modifying the class itself
- /guides/ruby-dependency-injection/. Dependency injection and the observer pattern share a common goal: keeping classes loosely coupled