Metaprogramming and building DSLs

define_method, send and public_send, method_missing with respond_to_missing?, class macros, refinements, and when to stop.

Dynamic methods

class Report
  ATTRS = %i[title author total].freeze

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

  def method_missing(name, *args, &block)
    if name.to_s.start_with?("find_by_")
      field = name.to_s.delete_prefix("find_by_")
      return nil unless ATTRS.include?(field.to_sym)
      @records.find { |r| r.public_send(field) == args.first }
    else
      super
    end
  end

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

report = Report.new
report.public_send(:title=, "Q3")
report.respond_to?(:find_by_author)   # => true
  • define_method takes a block, so it can close over locals - unlike class_eval with a string, which does not and is also a code-injection risk.
  • Always pair method_missing with respond_to_missing?, or duck typing, mocking and respond_to? all lie.
  • method_missing is slower and harder to debug than a real method; generate methods with define_method in a loop when the names are known.
  • public_send respects visibility; send bypasses it and should be reserved for tests or deliberate internals.
💡
Metaprogramming trades clarity for brevity. If a reader cannot find where a method is defined with a text search, the code has become hostile. Prefer explicit code and use these tools when they remove real duplication.

Class macros and DSLs

module Validatable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def validates(field, presence: false, format: nil)
      validations << { field: field, presence: presence, format: format }
    end

    def validations
      @validations ||= []
    end
  end

  def valid?
    self.class.validations.all? do |rule|
      value = public_send(rule[:field])
      next false if rule[:presence] && (value.nil? || value.to_s.empty?)
      next false if rule[:format] && !value.to_s.match?(rule[:format])
      true
    end
  end
end

class Signup
  include Validatable
  validates :email, presence: true, format: /\A[^@\s]+@[^@\s]+\z/
  validates :name, presence: true
end

Signup.new.tap { |s| s.email = "[email protected]"; s.name = "Ada" }.valid?   # => true
# a block-based DSL
class RouteSet
  def initialize(&block) = instance_eval(&block)

  def get(path, to:) = (@routes ||= []) << [:get, path, to]
  def post(path, to:) = (@routes ||= []) << [:post, path, to]

  def routes = @routes || []
end

set = RouteSet.new do
  get  "/books",     to: "books#index"
  post "/books",     to: "books#create"
end

Refinements and safe alternatives

module MoneyFormat
  refine Numeric do
    def to_money
      format("$%.2f", self / 100.0)
    end
  end
end

class Invoice
  using MoneyFormat
  def total = 12345.to_money      # "$123.45"
end

12345.respond_to?(:to_money)      # => false, outside the scope
TechniqueScopeRisk
Reopening a classGlobal, permanentBreaks other gems and Ruby upgrades
prepend a moduleGlobal, but explicit orderingLow - visible in the ancestor chain
RefinementLexical, file or class scopedLow, but confusing to newcomers
method_missingClass-wideSilent typos, poor stack traces

Before monkey-patching a core class, check whether a subclass or a small wrapper object would do. Global patches on String or Integer are the most common cause of a gem conflict that takes a day to find.

FAQ

When is method_missing justified?
For genuinely dynamic APIs: a proxy to a remote service, a configuration object reading arbitrary keys, an ORM's dynamic finders. When the set of names is known at load time, generate real methods instead.
What is a refinement good for?
Adding a method for one file without polluting the global namespace - formatting helpers, test-specific shorthands. The cost is that the behaviour is invisible from the class definition, so use it sparingly.

Objects, modules and exceptions Concurrency: threads, fibers and Ractors

Last refreshed 2026-09-18.