Skip to content

Ruby Metaprogramming — define_method send and Dynamic Code Generation Explained

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Ruby Metaprogramming. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby Metaprogramming enables runtime code generation using define_method for dynamic method creation, send for dynamic dispatch, method_missing for ghost methods, and eval for executing arbitrary strings as code.

What You'll Learn

  • Creating methods dynamically
  • Using send for dynamic dispatch
  • Runtime class manipulation
  • Building DSLs with metaprogramming

Why It Matters

Metaprogramming is Ruby's superpower. Durga Antivirus Pro uses metaprogramming for plugin systems and rule engines. Doda Browser uses it for configuration DSLs and dynamic form builders. Rails itself is built on metaprogramming — every belongs_to, validates, and scope call uses it.

Real-World Use

Rails' validations, associations, and scopes are all metaprogramming. Gems like Devise, CanCanCan, and PaperTrail use it extensively. ORMs generate methods based on schema.

flowchart LR
    A["Metaprogramming"] --> B["define_method"]
    B --> C["send"]
    C --> D["method_missing"]
    D --> E["DSL"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

define_method

Creates methods dynamically at runtime:

class DynamicMethods
  [:name, :email, :phone].each do |field|
    define_method("get_#{field}") do
      instance_variable_get("@#{field}")
    end

    define_method("set_#{field}") do |value|
      instance_variable_set("@#{field}", value)
    end
  end
end

obj = DynamicMethods.new
obj.set_name("Alice")
obj.set_email("alice@example.com")
puts obj.get_name   # Alice
puts obj.get_email  # alice@example.com

Bulk Method Definition

class ApiClient
  %w[get post put patch delete].each do |http_method|
    define_method(http_method) do |path, **params|
      request(method: http_method.upcase, path: path, body: params)
    end
  end

  private

  def request(method:, path:, body: {})
    puts "#{method} #{path} with #{body.inspect}"
  end
end

client = ApiClient.new
client.get("/users")
client.post("/users", name: "Alice")
client.delete("/users/1")
# GET /users with {}
# POST /users with {:name=>"Alice"}
# DELETE /users/1 with {}

send

Calls methods by name (string or symbol):

class Calculator
  def add(a, b) = a + b
  def subtract(a, b) = a - b
  def multiply(a, b) = a * b
  def divide(a, b) = a / b
end

calc = Calculator.new
operation = :add
puts calc.send(operation, 10, 5)  # 15

# Dynamic dispatch
["add", "subtract", "multiply"].each do |op|
  result = calc.send(op, 10, 5)
  puts "#{op}: #{result}"
end
# add: 15
# subtract: 5
# multiply: 50

send vs public_send

class SecureClass
  def public_method
    "public"
  end

  private

  def secret_method
    "secret"
  end
end

obj = SecureClass.new
puts obj.send(:public_method)    # public
puts obj.send(:secret_method)    # secret (bypasses privacy!)
# puts obj.public_send(:secret_method)  # NoMethodError

respond_to? and method_missing

class DynamicConfig
  def initialize
    @data = {}
  end

  def method_missing(name, *args, &block)
    key = name.to_s

    if key.end_with?("=")
      @data[key.chop] = args.first
    elsif @data.key?(key)
      @data[key]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    key = name.to_s
    @data.key?(key) || key.end_with?("=") || super
  end
end

config = DynamicConfig.new
config.api_key = "secret-123"
config.timeout = 30
puts config.api_key   # secret-123
puts config.timeout   # 30
puts config.respond_to?(:api_key)  # true

class_eval and instance_eval

# class_eval — adds methods to class
String.class_eval do
  def dash_case
    tr("_", "-")
  end
end

puts "hello_world".dash_case  # hello-world

# instance_eval — runs in context of instance
obj = "hello"
obj.instance_eval do
  def shout
    "#{self.upcase}!"
  end
end
puts obj.shout  # HELLO!

# class_eval with define_method
class User
  ATTRIBUTES = %i[name email role]

  ATTRIBUTES.each do |attr|
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{attr}
        @#{attr}
      end

      def #{attr}=(value)
        @#{attr} = value
      end
    RUBY
  end
end

user = User.new
user.name = "Alice"
puts user.name  # Alice

const_get and const_set

class NotificationFactory
  TYPES = { email: EmailNotification, sms: SMSNotification, push: PushNotification }

  def self.create(type, *args)
    klass = const_get(:"#{type.to_s.capitalize}Notification")
    klass.new(*args)
  rescue NameError
    raise "Unknown notification type: #{type}"
  end
end

class EmailNotification
  def send(msg) = puts "Email: #{msg}"
end

class SMSNotification
  def send(msg) = puts "SMS: #{msg}"
end

notif = NotificationFactory.create(:email, "alice@example.com")
notif.send("Hello!")  # Email: Hello!

Building a DSL

class Router
  class << self
    def routes
      @routes ||= []
    end

    def get(path, to:)
      routes << { method: :GET, path: path, handler: to }
    end

    def post(path, to:)
      routes << { method: :POST, path: path, handler: to }
    end

    def draw(&block)
      instance_exec(&block)
    end
  end
end

# DSL usage
Router.draw do
  get "/users", to: "users#index"
  get "/users/:id", to: "users#show"
  post "/users", to: "users#create"
end

Router.routes.each { |r| puts "#{r[:method]} #{r[:path]} -> #{r[:handler]}" }
# GET /users -> users#index
# GET /users/:id -> users#show
# POST /users -> users#create

ActiveRecord-style Macro

class BaseModel
  class << self
    def attributes(*names)
      names.each do |name|
        define_method(name) { instance_variable_get("@#{name}") }
        define_method("#{name}=") { |v| instance_variable_set("@#{name}", v) }
      end
    end

    def has_many(name, options = {})
      define_method(name) do
        klass = options[:class_name]&.constantize || name.to_s.classify.constantize
        klass.where("#{self.class.name.underscore}_id" => id)
      end
    end

    def validates(name, **options)
      @validations ||= []
      @validations << [name, options]
    end
  end

  def save
    self.class.instance_variable_get(:@validations)&.each do |name, options|
      value = send(name)
      if options[:presence] && (value.nil? || value.empty?)
        raise "Validation failed: #{name} can't be blank"
      end
    end
    true
  end
end

class Product < BaseModel
  attributes :name, :price, :sku
  validates :name, presence: true
  validates :sku, presence: true
end

product = Product.new
product.name = "Widget"
product.price = 9.99
puts product.save  # true

Common Mistakes

1. Eval with User Input

# Dangerous — code injection
def run_user_code(code)
  eval(code)  # Never do this with user input!
end

# Safe — use send or public_send
def call_method(obj, method_name, *args)
  obj.public_send(method_name, *args)
end

2. Forgetting respond_to_missing?

class Proxy
  def method_missing(name, *args)
    "handled #{name}"
  end
end

p = Proxy.new
p.respond_to?(:anything)  # false — breaks duck typing!

3. Performance of define_method in Hot Paths

# define_method in a loop called frequently
# Better to define statically or cache

# OK for setup-time (class definition)
# Bad if called repeatedly at runtime

4. Overusing Metaprogramming

# Unnecessary complexity — simple code is better
class Point
  [:x, :y].each { |a| attr_accessor a }
  # Just use: attr_accessor :x, :y
end

5. Method Collisions with method_missing

class HashConfig
  def method_missing(name, *args)
    if @data.key?(name.to_s)
      @data[name.to_s]
    else
      super
    end
  end

  # Now you can't call any real methods!
  # c.object_id returns @data["object_id"], not the real object_id
end

Practice Questions

1. What does define_method do?

Creates a method at runtime with the given name and block. define_method(:greet) { "Hello" } creates a greet method. Useful for creating families of methods dynamically.

2. What's the difference between send and public_send?

send can call any method including private/protected. public_send respects method visibility and only calls public methods. Use public_send for safer dynamic dispatch.

3. What is a DSL in Ruby?

A Domain-Specific Language that uses Ruby syntax to Express concepts naturally. Examples: validates :name, presence: true, scope :active, -> { where(active: true) }.

4. Why should you avoid eval with user input?

eval executes arbitrary Ruby code. User-provided strings can contain malicious code like system("rm -rf /"). Use safe alternatives like send or whitelist-based dispatch.

Challenge: Build a simple ActiveRecord-like query Builder using metaprogramming that supports where chaining, lazy evaluation, and dynamic attribute accessors.

Solution
class QueryBuilder
  def initialize(klass)
    @klass = klass
    @conditions = []
  end

  def where(**conditions)
    @conditions << conditions
    self
  end

  def method_missing(name, *args)
    if name.to_s.start_with?("find_by_")
      attrs = name.to_s.sub("find_by_", "").split("_and_")
      conditions = attrs.zip(args).to_h
      where(**conditions).first
    elsif @klass.instance_methods.include?(name)
      raise "Use execute instead"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?("find_by_") || super
  end

  def execute
    sql = "SELECT * FROM #{@klass.name.downcase}s"
    unless @conditions.empty?
      where_clauses = @conditions.flat_map { |c| c.map { |k, v| "#{k} = '#{v}'" } }
      sql += " WHERE #{where_clauses.join(" AND ")}"
    end
    puts sql
    # In real code: ActiveRecord::Base.connection.execute(sql)
  end
end

class Model
  def self.query
    QueryBuilder.new(self)
  end
end

class User < Model; end

# Usage
User.query.where(name: "Alice", active: true).execute
# SELECT * FROM users WHERE name = 'Alice' AND active = true

User.query.find_by_email("alice@example.com")
# SELECT * FROM users WHERE email = 'alice@example.com'

FAQ

{{< faq question="Is metaprogramming slow?" >}} define_method is fast (only slightly slower than regular methods). send has minimal overhead. method_missing is slower because it requires a failed method lookup first. eval is slowest and should be avoided. {{< /faq >}}

{{< faq question="Should I use metaprogramming in production code?" >}} Yes, but sparingly. Rails uses it extensively. The rule: use metaprogramming when it reduces repetition significantly and the pattern is obvious. Avoid it when simple code suffices. {{< /faq >}}

{{< faq question="How do I debug metaprogramming code?" >}} Use obj.methods, obj.instance_methods, obj.private_methods, and obj.method(:name).source_location to inspect dynamically created methods. Method#parameters shows parameter info. {{< /faq >}}

{{< faq question="What is the difference between class_eval and instance_eval?" >}} class_eval runs in the context of a class, defining instance methods. instance_eval runs in the context of a specific instance, defining Singleton methods. {{< /faq >}}

{{< faq question="Can I undefine a dynamically created method?" >}} Yes. Use remove_method(:method_name) to remove from the class, or undef_method(:method_name) to prevent it from being called via inheritance. {{< /faq >}}

Try It Yourself

# metaprogramming_demo.rb

class AttributeBuilder
  def self.build_attributes(*names)
    names.each do |name|
      define_method(name) { instance_variable_get("@#{name}") }
      define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
      define_method("#{name}?") { !!send(name) }
    end
  end
end

class Task < AttributeBuilder
  build_attributes :title, :completed, :priority

  def initialize(title = nil, completed: false, priority: :medium)
    @title = title
    @completed = completed
    @priority = priority
  end

  def to_s
    status = completed? ? "[x]" : "[ ]"
    "#{status} #{title} (priority: #{priority})"
  end
end

task = Task.new("Learn metaprogramming", completed: false, priority: :high)
puts task.title       # Learn metaprogramming
puts task.completed?  # false
puts task.to_s        # [ ] Learn metaprogramming (priority: high)
task.completed = true
puts task.completed?  # true
puts task.to_s        # [x] Learn metaprogramming (priority: high)

What's Next

Now that you understand metaprogramming basics, learn how to build Domain-Specific Languages (DSLs) in Ruby.

Topic Description Link
Ruby DSL Creating fluent interfaces {{< ref "34-dsl" >}}
Ruby send/define_method Dynamic method calls {{< ref "35-send-define-method" >}}
Python Metaclasses Compare Python's metaprogramming Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro