Obsolete Observers
Dependency on the rails-observers gem, an extraction from Rails 4 that reintroduced the Observer pattern Rails had moved away from. The gem receives minimal maintenance and blocks major Rails version upgrades because its internals couple to removed APIs.
Before / After
# Gemfile
gem 'rails-observers'
# config/application.rb
config.active_record.observers = :user_observer
# app/models/user_observer.rb
class UserObserver < ActiveRecord::Observer
def after_create(user)
WelcomeMailer.welcome(user).deliver_later
CrmSyncJob.perform_later(user.id)
end
def after_update(user)
SearchIndex.refresh(user) if user.saved_change_to_name?
end
end
# Blocks Rails upgrade, minimal maintenance,
# observers run inside the persistence transaction. # Use ActiveSupport::Notifications (built in)
# or the wisper / dry-events gem for richer events.
# app/models/user.rb
class User < ApplicationRecord
after_commit :publish_created, on: :create
after_commit :publish_updated, on: :update
private
def publish_created
ActiveSupport::Notifications.instrument(
'user.created', user_id: id
)
end
def publish_updated
ActiveSupport::Notifications.instrument(
'user.updated',
user_id: id,
changes: saved_changes
)
end
end
# Subscribers
ActiveSupport::Notifications.subscribe('user.created') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
WelcomeMailer.welcome(User.find(event.payload[:user_id])).deliver_later
CrmSyncJob.perform_later(event.payload[:user_id])
end Why this hurts
The rails-observers gem was extracted from Rails core during the 4.0 release cycle because the Observer pattern encouraged hidden global state and opaque call paths. The gem exists to support legacy code during migrations, not as a forward-looking architecture choice. Its internals couple to specific ActiveRecord internals (ActiveRecord::Observing, the observer registration callbacks) that have shifted shape across Rails versions, and each Rails upgrade requires corresponding observer updates that upstream maintainers ship slowly.
Observers fire synchronously inside the persistence transaction, which means every registered observer’s callback runs while the INSERT or UPDATE holds row locks. A slow Mailchimp API call inside after_create extends the transaction duration, multiplies lock contention, and increases the probability of deadlocks when concurrent operations touch the same rows. Unlike service-object orchestration where the caller controls which side effects run after commit, observers give the model no choice: the side effects run whether or not the caller cares about them.
Testing observers is operationally painful. The framework requires a global configuration flag (config.active_record.observers) that applies to the whole suite, not per-spec. Turning observers on for one test turns them on for the whole run, which means every spec incurs observer side effects whether or not it asserts on them. RSpec hooks can enable/disable observers per example, but the state is process-global and leaks across parallel test runners, producing flaky specs when one worker enables observers mid-run.
Replacing observers with after_commit callbacks that publish events through ActiveSupport::Notifications or a dedicated pub/sub gem (wisper, dry-events) shifts to after-commit semantics, so failing subscribers do not roll back the write. Subscribers register in initializers, not via global config, and can be stubbed per-spec without affecting the rest of the suite. Rails upgrades become tractable because ActiveSupport::Notifications is part of Rails core.
See also: Replacing Rails Observers with PubSub.
Get Expert Help
Inheriting a legacy Rails codebase with this problem? Request a Technical Debt Audit.