Skip to content

Ruby send and define_method — Dynamic Dispatch and Method Creation Explained

DodaTech Updated 2026-06-28 7 min read

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

Ruby send and define_method provide dynamic dispatch for calling methods by name at runtime and runtime method creation for generating methods programmatically based on data.

What You'll Learn

  • Using send for dynamic method calls
  • Creating methods with define_method
  • Performance considerations
  • Security best practices

Why It Matters

Dynamic dispatch is core to Rails and Metaprogramming. Doda Browser uses send for plugin loading. Durga Antivirus Pro uses define_method for scanner rule generation. Understanding these powers Rails' belongs_to, has_many, and validates macros.

Real-World Use

Rails' find_by_email, find_by_name_and_email — all generated by method_missing. ActiveRecord associations define methods dynamically. Form builders generate field methods.

flowchart LR
    A["send/define_method"] --> B["send"]
    B --> C["public_send"]
    C --> D["define_method"]
    D --> E["Dynamic Getters/Setters"]
    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:#dbeafe,stroke:#2563eb,color:#1e40af

send Basics

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

calc = Calculator.new

# Call by symbol
puts calc.send(:add, 5, 3)       # 8
puts calc.send(:subtract, 10, 4) # 6

# Call by string
puts calc.send("multiply", 6, 7) # 42

# Dynamic method name from variable
op = :add
puts calc.send(op, 5, 3)  # 8

send with Dynamic Arguments

def send_request(endpoint, method: :get, params: {})
  # Simulate API call
  puts "#{method.upcase} #{endpoint} with #{params}"
end

# Dynamic dispatch with hash
endpoint = "/users"
options = { method: :post, params: { name: "Alice" } }
send_request(endpoint, **options)  # POST /users with {:name=>"Alice"}

public_send for Safety

class SecureClass
  def public_method
    "public"
  end

  private

  def secret_method
    "secret"
  end
end

obj = SecureClass.new

# send bypasses privacy
puts obj.send(:public_method)   # public
puts obj.send(:secret_method)   # secret — bypasses private!

# public_send respects privacy
puts obj.public_send(:public_method)   # public
# obj.public_send(:secret_method)       # NoMethodError

define_method

class DynamicAccessors
  ATTRIBUTES = %i[name email phone role]

  ATTRIBUTES.each do |attr|
    define_method(attr) do
      instance_variable_get("@#{attr}")
    end

    define_method("#{attr}=") do |value|
      instance_variable_set("@#{attr}", value)
    end

    define_method("#{attr}?") do
      !!instance_variable_get("@#{attr}")
    end
  end
end

user = DynamicAccessors.new
user.name = "Alice"
user.email = "alice@test.com"
puts user.name     # Alice
puts user.email?   # true
puts user.phone?   # false

define_method with Blocks

class MathOperations
  [:add, :subtract, :multiply, :divide].each do |op|
    define_method(op) do |a, b|
      case op
      when :add then a + b
      when :subtract then a - b
      when :multiply then a * b
      when :divide then a / b
      end
    end
  end

  # More concise using send
  OPERATORS = {
    add: :+, subtract: :-, multiply: :*, divide: :/
  }

  OPERATORS.each do |name, operator|
    define_method(name) do |a, b|
      a.send(operator, b)
    end
  end
end

calc = MathOperations.new
puts calc.add(10, 5)       # 15
puts calc.multiply(3, 4)   # 12

Dynamic Finders Pattern

class Repository
  def initialize
    @records = []
  end

  def add(record)
    @records << record
  end

  def method_missing(name, *args)
    if name.to_s =~ /^find_by_(.+)$/
      field = $1
      @records.select { |r| r[field.to_sym] == args.first }
    else
      super
    end
  end

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

repo = Repository.new
repo.add({ name: "Alice", role: "admin" })
repo.add({ name: "Bob", role: "user" })
repo.add({ name: "Charlie", role: "admin" })

puts repo.find_by_name("Alice").inspect
# [{:name=>"Alice", :role=>"admin"}]
puts repo.find_by_role("admin").size  # 2

Delegator Pattern

class Delegator
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    if @target.respond_to?(name)
      puts "[DELEGATE] #{name}"
      @target.send(name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @target.respond_to?(name) || super
  end
end

class Printer
  def print(msg) = puts "Print: #{msg}"
  def scan = "scanning..."
end

delegator = Delegator.new(Printer.new)
delegator.print("Hello")  # [DELEGATE] print \n Print: Hello
result = delegator.scan   # [DELEGATE] scan
puts result               # scanning...

Performance Considerations

require "benchmark"

class Calculator
  def add(a, b) = a + b
end

calc = Calculator.new
n = 10_000_000

Benchmark.bm do |x|
  x.report("normal:") { n.times { calc.add(1, 2) } }
  x.report("send:  ") { n.times { calc.send(:add, 1, 2) } }
  x.report("public:") { n.times { calc.public_send(:add, 1, 2) } }
end

# Example output:
#                user     system      total        real
# normal:    0.421827   0.000295   0.422122  (0.422570)
# send:      0.553596   0.000249   0.553845  (0.554233)
# public:    0.575230   0.000260   0.575490  (0.575825)

send is about 30% slower than direct calls. Define methods statically when in hot paths.

Common Mistakes

1. send with User Input

# Dangerous — user can call any method
def call_method(method_name)
  obj.send(method_name)  # Could call :destroy, :delete_all
end

# Safe — whitelist
ALLOWED_METHODS = %i[name email age]
def call_safe(method_name)
  if ALLOWED_METHODS.include?(method_name.to_sym)
    obj.send(method_name)
  end
end

2. Forgetting public_send for APIs

# Bad — exposes private methods
def api_call(klass, method, *args)
  klass.send(method, *args)
end

# Good — respects privacy
def api_call(klass, method, *args)
  klass.public_send(method, *args)
end

3. define_method in a Loop Without Closure

# All methods reference the last value of i!
%w[upcase downcase capitalize].each do |_|
  define_method("transform_#{_}_wrong") do |str|
    str.send(_)
  end
end
# Actually this works because the block captures _ correctly in Ruby 2.0+

4. Overusing send Instead of Polymorphism

# Bad — manual dispatch
def process(shape)
  shape.send("calculate_#{shape.type}_area")
end

# Good — polymorphism
def process(shape)
  shape.area
end

5. Not Handling Missing Methods

# Bad — silent failure
result = obj.send(method_name) rescue nil

# Good — check first
if obj.respond_to?(method_name)
  obj.send(method_name)
end

Practice Questions

1. What does send do in Ruby?

Calls a method by name (string or symbol). obj.send(:method_name, args) is equivalent to obj.method_name(args) but the method name can be dynamic.

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

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

3. When should you use define_method?

When creating families of similar methods dynamically (accessors, finders, delegators). Define methods at class load time, not in hot paths.

4. Is send slower than direct method calls?

Yes, about 20-30% slower. For most applications this is negligible. Avoid send in performance-critical loops processing millions of iterations.

Challenge: Create a MethodRegistry that uses define_method to register and call named operations dynamically, supporting addition and removal of operations at runtime.

Solution
class MethodRegistry
  def initialize
    @operations = {}
  end

  def register(name, &block)
    @operations[name] = block
    self.class.define_method(name) do |*args|
      @operations[name]&.call(*args)
    end
  end

  def unregister(name)
    @operations.delete(name)
  end

  def method_missing(name, *args)
    if @operations.key?(name)
      @operations[name].call(*args)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @operations.key?(name) || super
  end
end

registry = MethodRegistry.new
registry.register(:double) { |x| x * 2 }
registry.register(:greet) { |name| "Hello, #{name}!" }

puts registry.double(5)      # 10
puts registry.greet("Alice") # Hello, Alice!
puts registry.respond_to?(:double)  # true

registry.unregister(:double)
puts registry.respond_to?(:double)  # false

FAQ

{{< faq question="Is send safe to use?" >}} Yes, when used with trusted method names. Never pass user input directly to send. Whitelist allowed methods or use public_send for public API calls. {{< /faq >}}

{{< faq question="Can I use send with blocks?" >}} Yes. obj.send(:method_name, *args) { |x| x * 2 } passes the block to the target method. The block is passed as a Proc parameter. {{< /faq >}}

{{< faq question="What's the difference between define_method and def?" >}} define_method creates methods at runtime with dynamic names. def creates methods at parse time with static names. define_method can capture surrounding scope (closures). {{< /faq >}}

{{< faq question="Can I undefine a method created with define_method?" >}} Yes. Use undef_method(:method_name) in the class to remove it, or remove_method(:method_name) to keep inherited versions. {{< /faq >}}

{{< faq question="How does Rails use send?" >}} Rails uses send extensively: find_by_* dynamic finders, association proxies, update_attribute, send in callbacks, and metaprogramming throughout Active Record and Action Pack. {{< /faq >}}

Try It Yourself

# send_demo.rb

class DynamicFormatter
  FORMATTERS = {
    upcase: ->(s) { s.upcase },
    downcase: ->(s) { s.downcase },
    reverse: ->(s) { s.reverse },
    titleize: ->(s) { s.split.map(&:capitalize).join(" ") }
  }

  FORMATTERS.each do |name, formatter|
    define_method(name) do |text|
      formatter.call(text)
    end
  end
end

formatter = DynamicFormatter.new
puts formatter.upcase("hello world")    # HELLO WORLD
puts formatter.titleize("ruby is fun")  # Ruby Is Fun
puts formatter.reverse("hello")         # olleh

# Using send dynamically
text = "dynamic dispatch"
[:upcase, :downcase, :reverse].each do |fmt|
  puts formatter.send(fmt, text)
end

What's Next

Now that you understand dynamic dispatch, explore method_missing for building ghost methods and dynamic proxies.

Topic Description Link
Ruby method_missing Ghost methods, dynamic proxies {{< ref "36-method-missing" >}}
Ruby DSL Building fluent interfaces {{< ref "34-dsl" >}}
Python getattr Compare Python's dynamic dispatch Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro