A Ruby gem that implements the Observer pattern for ActiveRecord models in Rails applications. ObservableModel provides a clean, organized way to respond to model lifecycle events (create, update, destroy) without cluttering your models with callback logic.
As Rails applications grow, model callbacks can become cluttered with business logic, side effects, and external service integrations. ObservableModel helps you:
- Separate concerns - Keep models focused on data and validation
- Organize side effects - Isolate external service calls, notifications, and async jobs
- Improve testability - Easily skip observers in tests with
skip_observersflag - Follow patterns - Implement the classic Observer pattern in a Rails-friendly way
- Maintain clarity - Know exactly where lifecycle-triggered logic lives
- π― Simple integration - Just include a module in your ActiveRecord models
- π Lifecycle hooks - Respond to before and after commit events for create, update, and destroy
- π« Skippable observers - Disable observers per-instance when needed
- π§© Convention-based - Auto-discovers observer classes (e.g.,
UserObserverforUser) - β‘ Before-commit callbacks - Enables observer actions to run before changes are committed
- β‘ After-commit callbacks - Ensures database transactions complete before running observer actions
- π§ͺ Test-friendly - Easy to bypass observers in test scenarios
- Ruby >= 3.2.0
- Rails >= 6.1
Add this line to your application's Gemfile:
gem 'observable_model'And then execute:
$ bundle install- Include the module in your ActiveRecord model:
class User < ApplicationRecord
include ObservableModel::Sources::ActiveRecordObservable
# Your model code...
end- Create an observer class:
ObservableModel uses a naming convention: for a model named User, create a UserObserver class.
# app/observers/user_observer.rb
class UserObserver < ObservableModel::Observers::Base
def pre_create
# Called before the record is created
end
def pre_update
# Called before the record is updated
end
def pre_destroy
# Called before the record is destroyed
end
def on_create_commit
# Called after a user is created and committed to the database
WelcomeMailer.welcome_email(@observable).deliver_later
AnalyticsService.track_signup(@observable)
end
def on_update_commit
# Called after a user is updated and committed
if @observable.saved_change_to_email?
EmailChangeNotifier.notify(@observable)
end
end
def on_destroy_commit
# Called after a user is destroyed and committed
CleanupService.remove_user_data(@observable.id)
end
endThe @observable instance variable contains the model instance that triggered the event.
To bypass observers for a specific operation:
user = User.new(name: "John Doe")
user.skip_observers = true
user.save # No observer callbacks will be triggeredOverride observer_class_name to use a non-conventional class name:
class User < ApplicationRecord
include ObservableModel::Sources::ActiveRecordObservable
def observer_class_name
"CustomUserObserver"
end
endWe recommend creating an app/observers directory in your Rails application:
app/
observers/
user_observer.rb
order_observer.rb
payment_observer.rb
Make sure to add this to your config/application.rb:
config.autoload_paths += %W(#{config.root}/app/observers)When you include ObservableModel::Sources::ActiveRecordObservable in your model, six callbacks are registered:
before_createβpre_createbefore_updateβpre_updatebefore_destroyβpre_destroyafter_create_commitβon_create_commitafter_update_commitβon_update_commitafter_destroy_commitβon_destroy_commit
On each event, the model looks up the observer class by convention, instantiates it with itself, and delegates the callback to it. If skip_observers is true, no observer is instantiated and all callbacks are silently skipped.
class UserObserver < ObservableModel::Observers::Base
def on_create_commit
UserMailer.welcome_email(@observable).deliver_later
end
endclass OrderObserver < ObservableModel::Observers::Base
def pre_destroy
raise "Cannot delete a completed order" if @observable.completed?
end
endclass OrderObserver < ObservableModel::Observers::Base
def on_update_commit
if @observable.saved_change_to_status?
OrderStatusNotifier.notify_customer(@observable)
AnalyticsService.track_status_change(@observable)
end
end
endclass AccountObserver < ObservableModel::Observers::Base
def on_destroy_commit
DeleteUserDataJob.perform_later(@observable.id)
AuditLog.create(action: 'account_deleted', account_id: @observable.id)
end
endAfter checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Run the test suite with:
$ bundle exec rake spec- Observer registration/configuration DSL
- Built-in async observer execution
- Observer metrics and monitoring hooks
Bug reports and pull requests are welcome on GitHub at https://github.com/CodeTectonics/observable-model. This project is intended to be a safe, welcoming space for collaboration.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the ObservableModel project's codebases and issue trackers is expected to follow the project's code of conduct.