Hooks and Callbacks in Ruby

· 6 min read · Updated March 27, 2026 · intermediate
ruby metaprogramming hooks callbacks

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