
Metaprogramming in Ruby is writing code that defines or invokes other code at runtime. Three methods cover most real-world uses: send calls a method by name, define_method creates methods dynamically, and method_missing handles calls to methods that don’t exist. Rails is built on these techniques, and they work unchanged in Ruby 3.4.
Metaprogramming has a reputation as an advanced, mystifying corner of Ruby — and going deep does take time, effort, and a different way of thinking. The good news: you don’t need to wade far into the metaprogramming waters to find techniques that solve real problems, even in normal Rails applications. We’ll define metaprogramming, see when it comes in handy, and work through each of those three methods (plus method_missing’s essential companion, respond_to_missing?).
When Is Metaprogramming Useful?
Maybe you’ve got a couple of models in your app that, while very similar, aren’t identical — they have different attributes and methods. But both need to be acted on in a job, and you don’t want to write what is essentially the same code more than once.
Maybe once (or twice), you hacked together a kludge masterpiece for a tight deadline on a new feature that got the job done, but you modified an existing model rather than creating a new one the way you would have if you’d had more time. Now that feature needs to be expanded, requiring you to do it the “right” way. This can be intimidating, especially if your codebase references your current implementation all over your app.
Or maybe you rely on a third-party gem that isn’t well-maintained, and you discover far too late that it’s got a bug in it — right in the middle of a line of code full of metaprogramming.
I’ve had all these things happen, and I solved them with some light metaprogramming — all without having to be an expert, and without too much effort. You can, too.
What Is Metaprogramming in Ruby?
Metaprogramming is a set of techniques for writing code that dynamically writes other code — code that works on code, rather than on data. This is great because it means we can write more dynamic, flexible, and adaptable code if the situation calls for it.
It can help DRY up portions of our code, dynamically define methods we might need, and write useful and reusable pieces of software (like gems!) to make our lives easier.
In fact, Rails uses metaprogramming on a large scale and to great effect. Any time anyone talks about Rails’ magic, they’re talking about its use of metaprogramming.
Caveats
Despite how useful metaprogramming is, it isn’t without its drawbacks.
One issue is readability: a little metaprogramming here and there can go a long way and likely won’t cause many headaches, but lots of it will hurt the readability of your code. Many Rails engineers are not very familiar with it, so relying on it too heavily can prove frustrating both to your future self and to others who need to work on your code.
Maintainability is also a concern; heavy use of metaprogramming can be mind-bending for even experienced Ruby developers. This means you should document your code. Ruby is an incredibly expressive, intuitive, and readable language, but if you’re doing things others might find confusing, you should leave comments. You should also use access modifiers — e.g., private, protected — the way you would with other methods.
Another potential source of trouble: monkeypatching. While monkeypatching (dynamic modification of a class — typically, a Rails core class) can be useful, it should also be used sparingly and carefully. If we’re not mindful, we can modify behavior in ways that, while perhaps solving one of our problems, can easily create dozens of others. For example, instead of fixing a small bug or adding a new, uniquely-named method, we might change the behavior of existing methods. Our gems — and Rails itself — call those methods too, with potentially disastrous consequences. Check out our post Responsible Monkeypatching in Ruby for more on monkeypatching.
Lastly, metaprogramming can make it hard to find things you’re looking for, especially if you didn’t write the code in the first place. If you find yourself trying to figure out how it’s possible CompanySpecificClass#our_special_method exists on an object but isn’t defined anywhere, metaprogramming could be the culprit (in this case, probably the use of define_method).
Dynamically defined methods have one more cost: they are hard to find in a backtrace when something breaks in production. If you monitor with AppSignal for Ruby, the full backtrace and error context show which metaprogrammed call raised.
Basic Ruby Metaprogramming Techniques
First up is send — what it does, and how it can help us DRY up our code.
Using send
Remember the earlier example (from ‘When Is Metaprogramming Useful?’) of a couple of similar models with different attributes and methods? In this case, both models need to be acted on in a job in a more or less identical manner.
Invoking send allows us to dynamically pass methods (along with arguments) to an object, without necessarily knowing what that object (or method) might be at the time we write the code.
First, a word of caution: send calls private and protected methods as readily as public ones. Its stricter sibling, public_send, respects encapsulation and raises NoMethodError when the method is private. If the method name comes from user input — or from anywhere outside your own code — prefer public_send, and save send for the times you deliberately need private access.
So how does it work? Pass the method’s name to the object you want to call, like this: my_object.send(:method_name). If you need to pass parameters/arguments, you can do so: my_object.send(:method_name, argument1, argument2...). send accepts parameters the same way other methods do, so you can use named arguments, too.
So rather than doing something verbose like this:
def sell_or_refund_or_return_item(item, action)
if action == "sell"
item.sell
elsif action == "refund"
item.refund
elsif action == "void"
item.void
elsif action == "return"
item.return
end
endWe can pass our method as an argument and call it with send:
def process_item(item, action)
return unless item.respond_to?(action)
item.send(action)
endNote: For brevity, we’re not raising an error if our object doesn’t respond_to? the action passed into these methods. In a real app, we’d probably want to raise an error to flag that something in our codebase is calling a non-existent method/attribute on the object. There’s a subtlety here, too: respond_to? returns false for private methods, so this guard silently skips a private action rather than raising — one more reason to reach for public_send when the method name comes from outside your code.
send: An Example Scenario
Consider the following example. We’ve got a Purchase model, representing a given purchase from an online store. That purchase could be anything from a single item of one type to many different items.
In the instance here, we’re interested in people who bought a particular subscription, which may contain tickets to events (purchase_events), and/or vouchers for products (purchase_products).
Products and events are different things, but there’s a lot of overlap, like price, status (sold, returned), etc. In this example, we will refund all instances of a particular item from a subscription, because customers were unable to redeem their purchase (an event was rained off, a company we source from ran out of a collectible, etc.).
class RefundAllInstancesOfItemJob
def perform(item)
purchases = Purchase.joins(:subscriptions)
.includes(:purchase_events, :purchase_products)
.where(subscriptions: { id: item.subscription_id, status: "active" })
association, foreign_key, foreign_key_value = item.is_a?(PurchaseEvent) ? [:purchase_events, :event_id, item.event_id] : [:purchase_products, :product_id, item.product_id]
purchases.find_each do |purchase|
purchase.send(association)
.where(purchase_id: purchase.id)
.where(foreign_key => foreign_key_value)
.each { |item| item.update(status: "refunded") }
end
end
endWhat have we done here? Ordinarily, we might have ended up writing some if/else logic and duplicating most of this code or even two separate jobs. Instead, we’ve dynamically sent the appropriate association and foreign key to objects within our query, keeping our code DRY.
We’ve laid out the keys and associations here explicitly, but they could be passed in as arguments if we set up our call to the job differently. Our customers have now been refunded for the particular item(s) in this subscription cycle! (Let’s assume there’s a callback on those models that refunds the user when the item is marked "refunded".)
Using define_method
Now for define_method, and how it can help with our earlier example, where we moved data off a model it never belonged on and over to a new dedicated model.
Say we currently have a Presentation model. When we first added video capabilities to our app, we did it because our biggest customer had to play a single, long video about their business to their shareholders. We had a tight deadline, so rather than create a new, dedicated model, we tacked a few columns onto our Presentation table, and it got the job done.
Now, though, because we’ve advertised our app’s recording and streaming capabilities, other clients are interested — and they need more than one video per Presentation. Unfortunately, we’re still in a bit of a time crunch and have to roll this out fast.
We decide the quickest way to accomplish this without changing tons of code is to:
- Create a new
VideoAssetmodel - Move existing columns over from
Presentation - Set a boolean
current_assetwhich will update when our clients select the video they want
class Presentation
has_many :video_assets
def current_video_asset
video_assets.find_by(current_asset: true)
end
# Replace our dropped columns as new instance methods on Presentation
# We've used the safety operator (&) on send the way we would any other method with a potential nil return value
["status", "playback_id", "asset_id"].each do |column_suffix|
define_method(column_suffix) do
current_video_asset&.send(column_suffix)
end
end
endWhat’s going on here? We’re dynamically defining methods on our Presentation model to replace the columns we dropped and moved over to VideoAsset. The existing calls in our codebase to @presentation objects will now first find the current VideoAsset associated with our Presentation and then call the corresponding column. We don’t have to go around updating controllers and views everywhere!
define_method builds each method’s body from the block you hand it — and it accepts a lambda or proc, too. If blocks, procs, and lambdas feel fuzzy, our introduction to lambdas in Ruby covers the other half of what makes dynamic definition work.
The methods defined here could also be built with Active Support’s delegate helper, which abstracts away the metaprogramming implementation:
class Presentation
has_many :video_assets
delegate :status, :playback_id, :asset_id, to: :current_video_asset
def current_video_asset
video_assets.find_by(current_asset: true)
end
endUsing method_missing
The last of the three is method_missing. It does pretty much what you’d expect, given its name — it allows you to account for methods that don’t exist but are called on an object or class. We’ll turn methods we’ve placed in our classes into methods that test for truthiness.
class User
def method_missing(method_name, *args)
if method_name.end_with?("?")
regular_method = method_name.to_s.sub("?", "")
result = self.send(regular_method, *args)
result.present? && result != 0
else
super
end
end
endSo, what’s happening here?
Well, we’ve recreated an ActiveRecord feature for our User model. Any column that exists on a table in Rails has an identically-named method ending in a ? added to instances of its corresponding class. We’ve done something similar with our User class — any undefined instance method called on a user object ending in a ? will be tried against a method of the same name without its question mark, and turn the result into a boolean.
So, for example, @user.purchase_totals? will (rather than return a decimal representing the total amount of money a user has spent in our app) return true if the number is nonzero — otherwise, false.
The present? check accounts for things like empty strings and arrays, which would otherwise count as true if we’d used a double-bang to assess truthiness.
One portability fix from an earlier version of this post: the example used to call ends_with?, an Active Support alias that only exists inside Rails. In plain Ruby, it raises undefined method 'ends_with?' for an instance of Symbol (NoMethodError), with a did-you-mean pointing at the core end_with? used here — which works in Rails and plain Ruby alike. (present? is Active Support too, so outside Rails, swap it for a truthiness check of your own.)
If there’s no match, we default to calling super, resulting in the expected behavior: a NoMethodError.
Using respond_to_missing?
Our User now answers purchase_totals?, but ask it and it will deny everything: @user.respond_to?(:purchase_totals?) returns false, even though the call works. respond_to? never consults method_missing — it consults respond_to_missing?. That breaks duck typing, and it breaks code (like our own process_item guard) that checks capabilities before calling. So the rule is: every method_missing ships with a matching respond_to_missing?.
Here’s the pattern on a trimmed-down Item class that runs in plain Ruby — the array check stands in for Active Support’s present?:
class Item
def price
100
end
def method_missing(method_name, *args)
if method_name.end_with?("?")
result = send(method_name.to_s.sub("?", ""), *args)
![nil, false, 0, ""].include?(result)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.end_with?("?") || super
end
private
def secret
"for internal use only"
end
end
item = Item.new
item.price? # => true
item.respond_to?(:price?) # => true — false without respond_to_missing?Three failure modes are worth knowing here, each reproduced on Ruby 3.4.10:
- The
superpath:item.sell_fastmatches nothing, falls through tosuper, and raisesundefined method 'sell_fast' for an instance of Item (NoMethodError)— the same error a class withoutmethod_missingwould raise, which is exactly why theelsebranch callssuperinstead of swallowing the call. - Private methods:
item.send(:secret)cheerfully returns the private method’s value, whileitem.public_send(:secret)raisesprivate method 'secret' called for an instance of Item (NoMethodError).respond_to_missing?’s second argument,include_private, exists so your dynamic methods can honor the same distinction. - The recursion footgun: a
method_missingthat itself calls a missing method — a misspelled helper, say — re-entersmethod_missingand recurses until Ruby raisesstack level too deep (SystemStackError). If yourmethod_missingblows the stack, check it for typos first.
More Advanced Metaprogramming Techniques in Ruby: An Overview
We’ve covered some useful methods that can be used sparingly to help us out in everyday situations. But what about more advanced uses of metaprogramming? What else is it good for?
A few more core techniques are worth knowing by name. Each of these runs as shown on Ruby 3.4.
class_eval reopens an existing class and evaluates code in its context — the engine behind most monkeypatching:
String.class_eval do
def shout
upcase + "!"
end
end
"hello".shout # => "HELLO!"instance_variable_get and instance_variable_set read and write an object’s instance variables from the outside, no accessors required:
item = Item.new
item.instance_variable_set(:@status, "sold")
item.instance_variable_get(:@status) # => "sold"define_singleton_method defines a method on one object rather than its whole class:
item.define_singleton_method(:featured?) { true }
item.featured? # => trueAnd where does all this power end up? A few places:
- DSLs: As mentioned, ActiveRecord, the ORM (and DSL) that ships with Rails, relies heavily on metaprogramming. That’s where all those automatic methods on our objects come from. Every column we add to a table becomes two methods on our instances —
columnandcolumn=(andcolumn?for those who were paying attention!). It isn’t magic that’s making this happen, it’s metaprogramming — in fact, the use of the samemethod_missingmethod we talked about earlier! Metaprogramming is also responsible for Rails’ ability to automatically create methods likefind_by_first_nameandfind_by(first_name: "name")— dynamic finders that still work on ActiveRecord 8.1. - Gems: If you’re building a gem, chances are you’ll want it to be flexible, adaptable, and to work with more than one specific Rails stack. Most of the gems you regularly use have at least some degree of metaprogramming, and many use a lot to achieve the level of flexibility they offer.
- Dynamic APIs: Metaprogramming can help us design dynamic APIs by allowing us to define methods on the fly based on runtime information, rather than hardcoding every possible method. For example, a RESTful API might expose resources with dynamic paths and attributes based on the data model of the underlying application (this may sound familiar — Rails’ routing system does this). We can generate methods for each resource at runtime based on the database schema and dynamically respond to HTTP requests.
- Frameworks: Rails wouldn’t be Rails without metaprogramming. While it’s a particularly magical framework, almost any framework you build will need plenty of metaprogramming. If you’re going to build your own (presumably far more lightweight) framework, you’ll need metaprogramming, too.
Wrapping Up
We’ve covered some real ground here: dynamic method definition with define_method and method_missing, dynamic invocation with send and public_send, and why every method_missing ships with a respond_to_missing?.
Along the way, we weighed metaprogramming’s pitfalls — readability, maintainability, and searchability — and touched on more advanced use cases like writing gems, DSLs, and frameworks.
Further Learning
There are a lot of resources out there for taking Ruby metaprogramming further.
The official Ruby documentation covers the core of what we’ve used in depth — start with BasicObject#method_missing and Module#define_method.
A couple of paid courses include:
- Ruby Metaprogramming — Complete Course from Udemy
- Chris Oliver’s comprehensive Advanced Ruby: Behind the Magic. In addition to digging deep into Rails, this also has a section on metaprogramming and DSLs.
An oft-recommended book is Metaprogramming Ruby 2: Facets of Ruby.
I hope you found this post useful. Thanks for reading!
P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Frequently asked questions
- What is metaprogramming in Ruby?
- Metaprogramming is writing code that defines or invokes other code at runtime. In Ruby, that means techniques like calling methods dynamically with send, defining methods on the fly with define_method, and handling calls to undefined methods with method_missing. Rails relies heavily on all three.
- What is the difference between send and public_send?
- send calls a method by name and reaches private and protected methods. public_send raises NoMethodError when the method is private, respecting encapsulation. Prefer public_send when the method name comes from outside your code, and keep send for deliberate private access.
- When should I use define_method instead of method_missing?
- Use define_method when you can list the method names ahead of time. The methods really exist, so respond_to? works and backtraces stay readable. Reserve method_missing for names you cannot know in advance, and always pair it with respond_to_missing? and a super fallback.
- Why does method_missing need respond_to_missing?
- method_missing changes what an object answers to without telling Ruby. Without a matching respond_to_missing?, respond_to? returns false for calls that work, which breaks duck typing and any code that checks capabilities before calling. Defining both keeps dynamically handled methods discoverable.
Published , Updated
Wondering what you can do next?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Daniel Lempesis
Our guest author Daniel is a software engineer passionate about Ruby, Rails and software development in general. Most days he can be found squashing bugs or working on building out a new feature.
All articles by Daniel LempesisBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!


