rubyguides

Refinements — Scoped Monkey Patching

Monkey patching is a fact of life in Ruby, but without refinements scoped changes can leak into unexpected corners of your codebase. Sometimes you just need String to have a method it doesn’t, yet patching String globally affects every piece of code in your project, including code you don’t control. Refinements solve this with a scoped approach: your patches stay confined to a specific lexical scope, and the rest of the codebase never sees them.

Defining a Refinement

Use Module#refine inside a module to define what you’re changing:

module StringExtensions
  refine String do
    def shout
      "#{self.upcase}!"
    end

    def title_case
      split.map(&:capitalize).join(" ")
    end
  end
end

refine creates an anonymous module holding your modifications. It doesn’t affect anything until you activate it. The refinement definition itself is purely declarative; it says what you want to change, but it does not actually change anything yet. Think of it as writing a patch that sits on the shelf waiting to be applied. The separation between definition and activation is what gives refinements their scoped nature, because the same definition can be activated in one file while leaving another file untouched.

Activating with using

using StringExtensions

"hello world".shout       # => "HELLO WORLD!"
"hello world".title_case  # => "Hello World"

using activates the refinements in the current lexical scope. From that line to the end of the file, or to the end of the current class or module definition, the modified methods are available. This is the key difference from a global monkey patch: the activation point is visible in the source code, and you can see exactly which part of the file has access to the modified methods. Everything outside that lexical boundary continues to use the original behavior.

Outside that scope, the original String behavior applies:

using StringExtensions

"hello".shout  # => "HELLO!"

# This file doesn't have `using`, so refinements are not active here
require "./other_file"  # other_file sees original String

Scope Rules

Refinements are lexical. That has several practical consequences you should be aware of before relying on them in a larger codebase. The lexical rule means that what you see on the screen determines what the Ruby interpreter sees. Moving code to a different file or a different position within the same file can change whether a refinement is active. This is both the feature’s greatest strength and its most common source of confusion.

You can’t activate them inside a method:

class MyClass
  def activate_shout
    using StringExtensions  # SyntaxError: refinements cannot be used here
  end
end

Refinements are active until the end of the class or file:

A refinement stays active from the using line to the closing end of the surrounding module, class, or file. Once that scope ends, the original methods take over again. This is true even if the refined class is used indirectly through other objects, since the refinement only affects code that appears lexically after the using call within the same scope block.

module MyHelpers
  using StringExtensions

  def self.process(text)
    text.shout  # refinement is active here
  end
end

"hello".shout  # NoMethodError — outside the module scope

Reopening a class doesn’t reactivate your refinement:

The lexical rule means that reopening a class later in the same file still keeps the refinement active, as long as the using statement sits at the same top-level scope. This can be surprising if you are used to thinking of classes as independent containers. The refinement does not belong to the class; it belongs to the lexical scope of the file.

module MyExtensions
  refine Integer do
    def factorial
      self <= 1 ? 1 : self * (self - 1).factorial
    end
  end
end

using MyExtensions

class Foo
  5.factorial  # works — inside same file as `using`
end

class Foo
  10.factorial  # still works — same top-level lexical scope
end

Method lookup order

When Ruby looks up a method on an object, it checks in this order:

  1. Refinements (most recently activated takes priority)
  2. Prepended modules
  3. The class itself
  4. Ancestors

Refinements always win over prepended modules, and prepended modules always win over the class. This means your refinement takes precedence over both a prepend and the original method.

Refinements in Practice

One common use is test helpers or debugging tools that want to add methods without polluting global scope:

module DebugRefinements
  refine Hash do
    def inspect_keys(*keys)
      select { |k, _| keys.include?(k) }.inspect
    end
  end
end

using DebugRefinements

{ name: "Alice", age: 30, role: "admin" }.inspect_keys(:name, :role)
# => "{:name=>\"Alice\", :role=>\"admin\"}"

Gotchas

While refinements are a clean way to scope your patches, they come with a few surprises that are worth knowing before you commit to using them in a project. The most common ones stem from the lexical nature of using and the fact that refinements only affect code after the activation point.

Code before using doesn’t get the refinement:

"hello".shout  # NoMethodError
using StringExtensions
"hello".shout  # "HELLO!"

Refinements don’t affect BasicObject methods. You can’t refine methods like object_id or send that exist on BasicObject.

Performance is slower than direct method calls. Each method lookup in a refined scope adds overhead. Negligible in most application code, measurable in tight loops.

Calling self inside a refinement uses the refinement’s version:

module MyRefinements
  refine String do
    def shout
      # If upcase is also refined, this calls the refined upcase
      "#{upcase}!"
    end
  end
end

When to use refinements

Refinements are worth reaching for when:

  • You’re writing a test helper that adds debugging methods — keep it in the test file
  • You’re building a DSL and want clean syntax without polluting the global namespace
  • You need to temporarily patch a gem’s behavior without affecting other parts of your codebase

For permanent application-level patches to core classes, a standard monkey patch in an initializer is simpler and faster.

Key takeaways

  • Refinements let you scope changes to a specific file, class, or module instead of changing behavior globally.
  • using is lexical, so the activation point matters as much as the refinement definition itself.
  • They are useful when you need a safe local override for tests, DSLs, or experiments.
  • Refinements are not a replacement for every monkey patch. Some changes are clearer as ordinary class extensions.
  • If the code would be easier to understand with a plain helper method, prefer the helper.

Refinements versus monkey patching

Monkey patching changes a class for the entire process. That can be useful in an application where you own the whole codebase, but it is risky in shared libraries or larger teams because the change affects every call site. Refinements keep the same basic idea, but they narrow the blast radius.

That narrow scope is the main reason refinements exist. You can experiment with a better API, create a test-only helper, or make a temporary compatibility layer without surprising unrelated code. The trade-off is that the rules are stricter and the syntax can be easier to miss in a long file.

If you are tempted to patch a core class globally, ask whether the behavior is truly universal. If it is not, a refinement is often the safer choice.

Common mistakes

The first common mistake is forgetting where using applies. Because refinements are lexical, moving code into another file or reopening a class can change whether the refinement is active. If a method suddenly stops working, check the exact location of the using statement first.

The second mistake is overusing refinements for application code that would be easier to understand as a helper or a plain module method. Refinements are powerful, but they are also easy to hide in a codebase. Use them when scope control matters, not just because the syntax looks neat.

The third mistake is expecting refinements to behave like global patches. They do not. Code outside the lexical scope still sees the original method, and that is the point. If you need the behavior everywhere, a refinement is probably the wrong tool.

Frequently asked questions

Are refinements good for production code?

Yes, when you need local scope control. They are especially helpful in libraries, internal DSLs, and test helpers. For broad application-wide changes, a different approach is usually easier to maintain.

Can I use refinements with gem code?

You can, but the lexical rules still apply. If the gem code lives in a different file or scope, the refinement may not be active there. That makes refinements much more predictable than monkey patching, but also less convenient.

Do refinements improve performance?

No. They trade convenience and scope control for a small amount of lookup overhead. In most code that overhead is tiny, but the main reason to use refinements is safety, not speed.

Conclusion

Refinements are a good fit when you want to change behavior without changing the whole world. They keep patches local, make the intent more explicit, and help avoid the hidden coupling that global monkey patches can create.

If a helper method or a small wrapper class solves the problem cleanly, use that first. If you need local method changes with tight scope, refinements give you a clear Ruby-native option.

A practical rule of thumb

Use refinements when you want the change to stay local and obvious. That is especially helpful in tests, DSLs, and migration code where a global monkey patch would be too risky. If the behavior belongs everywhere, a normal class extension is usually simpler. If the behavior belongs nowhere except one file, a refinement is a good fit.

In practice, the best refinement is usually the one you can remove later without hunting through the rest of the application. That makes it a safer temporary tool than a monkey patch and a better fit for experiments that should not leak into unrelated code.

See Also