Architecture

Replacing Rails Observers with PubSub Architecture

BLUF (Bottom Line Up Front): ActiveRecord::Observer was extracted from Rails core in version 4.0 because it encouraged hidden, global state manipulation. If your legacy application still relies on the rails-observers gem, you are blocking future framework upgrades. The modern, decoupled alternative is implementing a Publisher/Subscriber (PubSub) architecture using the wisper gem.

Phase 1: The Observer Anti-Pattern

Glossary entry: Obsolete Observers.

Observers listen to model lifecycles globally. They suffer from the same issues as standard callbacks, but are even worse because the logic is hidden in a completely different file, making the execution path invisible to developers reading the model.

Synthetic Engineering Context: The Hidden Execution

# Legacy Rails 3/4 Observer
class UserObserver < ActiveRecord::Observer
  def after_create(user)
    # This executes globally every time ANY user is created, 
    # even during factory generation in tests.
    WelcomeEmailJob.perform_later(user.id)
  end
end

# config/application.rb
# Observers had to be registered globally
config.active_record.observers = :user_observer

When migrating to Rails 6 or 7, maintaining the rails-observers dependency becomes a major technical liability.

Phase 2: Implementing Wisper PubSub

The wisper gem allows you to broadcast domain events explicitly from your controllers or service objects, rather than hooking into global database events.

Execution: The Publisher

Include the Wisper::Publisher module in your service object.

# app/services/user_creator.rb
class UserCreator
  include Wisper::Publisher

  def call(params)
    user = User.new(params)
    
    if user.save
      # Explicitly broadcast a domain event
      broadcast(:user_created, user)
    else
      broadcast(:user_creation_failed, user.errors)
    end
  end
end

Execution: The Subscriber

Create a plain Ruby class to handle the event. It does not need to inherit from any Rails-specific classes.

# app/subscribers/email_subscriber.rb
class EmailSubscriber
  def user_created(user)
    WelcomeEmailJob.perform_later(user.id)
  end
end

Execution: Wiring it together

You attach the subscriber to the publisher at the point of execution (e.g., the controller).

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    creator = UserCreator.new
    
    # Subscribe the listener to this specific execution
    creator.subscribe(EmailSubscriber.new)
    
    creator.call(user_params)
    # ... render response
  end
end

Phase 3: Next Steps & Risk Mitigation

Transitioning to PubSub requires shifting the team’s mindset from “Database CRUD hooks” to “Domain Events”. You must ensure that all code paths creating a resource are updated to use the Publisher service; otherwise, listeners will not be triggered.

Need Help Stabilizing Your Legacy App? Removing obsolete gems and architectural anti-patterns is critical for a successful Rails upgrade path. Our team at USEO specializes in modernizing event-driven logic in legacy monoliths.

Contact us for a Technical Debt Audit