Hooks and Callbacks in Ruby
Ruby gives you the ability to intercept events that happen during your program’s lifetime. When a module gets mixed into a class, when a class gets subclassed, or when someone calls a method that doesn’t exist, Ruby can notify you. You define a special method and the interpreter calls it at the right moment. No event emitters, no observer objects. Just methods with reserved names.
These reserved methods are called hooks or callbacks, and they power nearly every Ruby framework you’ve used. ActiveRecord, RSpec, Rake, and Sinatra all rely on them. This guide covers every important hook, when it fires, and what you can do with it.
The included Callback — Module Inclusion
When you mix a module into a class using include, Ruby immediately calls self.included on the module, passing the including class as an argument.
module Printable
def self.included(base)
puts "#{base} just included #{self}"
end
def print
puts content
end
end
class Report
include Printable
end
# Output: Report just included Printable
The included callback is the workhorse of Ruby’s DSL-building. The most common pattern uses it to add class methods to whatever class includes your module:
module MacroMethods
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def foo
puts "foo!"
end
end
end
class MyClass
include MacroMethods
end
MyClass.foo # => "foo!"
Because include is processed at class definition time, included fires during the class definition itself. Frameworks like ActiveRecord use this to set up validation rules, associations, and callbacks the moment your model class is defined.
The extended Callback — Module Extension
Where include gives a class instance methods, extend gives an object (or a class) singleton methods. When extend is called, Ruby fires self.extended:
module SayHello
def self.extended(obj)
puts "Extended onto #{obj}"
end
def say_hello
"Hello, #{self}!"
end
end
obj = Object.new
obj.extend(SayHello)
# Output: Extended onto #<Object:0x00007f9a2c03a418>
obj.say_hello # => "Hello, #<Object:0x00007f9a2c03a418>!"
When you extend a class itself (rather than an instance), the class gets the module’s methods as class methods:
class User
end
module AdminFeatures
def self.extended(klass)
puts "AdminFeatures added to #{klass}"
end
def admin_panel
"Admin panel for #{self}"
end
end
User.extend(AdminFeatures)
User.admin_panel # => "Admin panel for User"
This pattern lets you attach capabilities to specific objects at runtime without affecting other instances of the same class.
The prepended Callback — Module Prepending
prepend works like include, but with a critical difference in method lookup order. When a module is prepended, Ruby places it before the class in the method lookup chain. This means the module’s methods take priority and can call super to reach the original:
module UpcaseName
def self.prepended(base)
puts "#{base} prepended #{self}"
end
def name
"PREFIX: #{super}"
end
end
class Person
prepend UpcaseName
def name
"Alice"
end
end
p = Person.new
p.name # => "PREFIX: Alice"
With include, if both the module and class define name, the class wins. With prepend, the module wins. For decorating or wrapping existing methods, this is much cleaner than alias-chaining:
module Timestamper
def self.prepended(base)
base.alias_method :original_initialize, :initialize
end
def initialize
puts "Object being created"
original_initialize
end
end
ActiveSupport uses prepend extensively for method wrapping. Before Module#prepend existed, Rails used alias_method_chain which was essentially manual prepend via aliasing.
The inherited Callback — Class Inheritance
When a class subclasses another class, Ruby calls self.inherited on the parent class, passing the new subclass:
class Animal
def self.inherited(subclass)
puts "#{subclass} inherited from #{self}"
@subclasses ||= []
@subclasses << subclass
end
end
class Dog < Animal; end
class Cat < Animal; end
Animal.subclasses # => [Dog, Cat] (Ruby 2.4+)
The inherited hook is the foundation of auto-registration patterns. A plugin system can track every class that inherits from a base without requiring manual registration calls:
class Plugin
@plugins = []
def self.inherited(subclass)
@plugins << subclass
puts "Registered: #{subclass}"
end
def self.registered_plugins
@plugins
end
end
class MyPlugin < Plugin; end
class AnotherPlugin < Plugin; end
Plugin.registered_plugins # => [MyPlugin, AnotherPlugin]
In Rails, ApplicationRecord uses inherited to ensure every model gets the same base setup automatically. You subclass ApplicationRecord, and the framework hooks handle the rest.
Method Missing — Intercepting Undefined Calls
Every time you call a method Ruby can’t find in the normal lookup chain, it calls method_missing on the receiver, passing the method name and any arguments. To understand how blocks and Ruby blocks, procs, and lambdas relate to this, it helps to know that method_missing receives whatever arguments the caller passed, including any block attached via &:
class HashLike
def initialize(data = {})
@data = data
end
def method_missing(method_name, *args, &block)
if method_name.to_s.end_with?("=")
key = method_name.to_s.chomp("=").to_sym
@data[key] = args.first
else
@data[method_name.to_sym]
end
end
end
h = HashLike.new
h.user_name = "Alice"
h.user_name # => "Alice"
method_missing is powerful but you need a companion method. When Ruby evaluates respond_to?(:user_name), it checks the method table and returns false unless you override respond_to_missing?:
class HashLike
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.end_with?("=") || @data.key?(method_name.to_sym)
end
end
h = HashLike.new
h.respond_to?(:user_name=) # => true
h.respond_to?(:foo) # => false
Without respond_to_missing?, code using respond_to? before calling dynamic methods gets incorrect information. Always implement both or you’ll break send, delegate, and other metaprogramming tools.
Tracking Method Definitions
Ruby also fires callbacks when methods are added, removed, or undefined on a class or module. The most commonly used is method_added, which fires each time an instance method is defined:
class Tracker
def self.method_added(name)
puts "Instance method defined: #{name}"
end
def one; end
def two; end
end
# Output:
# Instance method defined: method_added
# Instance method defined: one
# Instance method defined: two
Note that method_added fires for itself when you define it on the class — that’s why you see method_added as the first logged name.
There are matching hooks for singleton methods and for removal/undefinition:
module Plugin
def self.included(base)
base.extend(PluginHooks)
end
module PluginHooks
def method_added(name)
puts "Method added: #{name}"
end
def method_removed(name)
puts "Method removed: #{name}"
end
end
end
These hooks are useful for building AOP-style instrumentation or for tracking what methods a class or module defines over time.
Putting It Together — A Self-Registering Service Pattern
Here’s a pattern that combines multiple hooks to build a self-registering service class. It uses included to set up class-level state, and inherited to automatically register any subclass:
module Registrable
def self.included(base)
base.extend(ClassMethods)
base.class_eval { @registry = [] }
end
module ClassMethods
def register(klass)
@registry << klass
end
def registered
@registry
end
end
def self.inherited(subclass)
subclass.class_eval { @registry = [] }
register(subclass)
end
end
class BaseService
include Registrable
end
class UserService < BaseService
def self.name
"UserService"
end
end
class PaymentService < BaseService
def self.name
"PaymentService"
end
end
BaseService.registered # => [UserService, PaymentService]
You can add new services without changing any registration code. The hooks handle it automatically.
See Also
- Ruby Blocks, Procs, and Lambdas — the foundational concepts behind hooks and callbacks
- Using define_method — dynamically defining methods that hooks can track
- instance_eval and class_eval — evaluating code in the context of classes and instances