Ruby Refinements Over Monkey Patches: 7 Patterns, Real Gotchas, and Runnable Code
Learn how Ruby refinements prevent monkey patch collisions with 7 practical patterns, runnable code examples, and testing strategies. Safer class changes start here.
I once watched a team lose a full day to a monkey patch. Someone had reopened String in an initializer to add a slugify method, and six months later that method collided with another team’s patch, and a checkout page started rendering prices with hyphens in them. Nobody could find the cause because the patch was invisible. It lived in a file nobody read anymore, quietly changing behavior across the entire application.
That week I started using refinements. Not everywhere, not as a religion, but as the tool I reach for when I need to change a class without changing it for everyone. This article is what I have learned since—seven patterns, some hard-won gotchas, and enough runnable code that you can copy, paste, and see the behavior for yourself.
I am going to explain this as simply as I can. No assumptions beyond basic Ruby. If you can write a class and a method, you can follow along.
First, the plain-English definition. A refinement is a change to a class that only applies where you explicitly switch it on.
An ordinary monkey patch looks like this:
class String
def squish
gsub(/\s+/, " ").strip
end
end
" hello world ".squish # => "hello world"
That patch is now live in every file of your program. Forever. Anyone can call it, even code that has no idea it exists.
A refinement does the same job but contains the change inside a box:
module Cleaner
refine String do
def squish
gsub(/\s+/, " ").strip
end
end
end
using Cleaner
" hello world ".squish # => "hello world"
Without the using Cleaner line, squish does not exist. Code that never opts in never sees it.
One detail that trips people up: using is not something you sprinkle inside methods. It applies from the point you call it to the end of the current file or module. So put it at the top. If you put it halfway down, the lines above it see the plain old class. I learned that one by staring at a failing test for twenty minutes.
Now the seven patterns.
Pattern One: Refining a Core Class
This is the classic use. You want a small behavior that Ruby or ActiveSupport does not give you. Say I am cleaning messy form input and I want Ruby’s own squish, which ActiveSupport has but plain Ruby does not:
module TextHelpers
refine String do
def squish
gsub(/\s+/, " ").strip
end
end
end
Then in the one file that needs it:
require_relative "text_helpers"
using TextHelpers
def clean(name)
name.squish
end
clean(" Ada Lovelace ") # => "Ada Lovelace"
Any other file that calls clean is fine, because clean does the refined work internally. But if a stranger file tries " hi ".squish, it gets a NoMethodError. Nothing leaked.
A rule I follow: you can only refine methods in classes where refinements can apply—regular classes and modules. You cannot refine an object singleton, and inside refine you cannot use super to reach a method that does not exist on the original class. In other words, you may rename behavior, but you cannot invent methods that were never defined on the underlying class through super chains. For new methods, just define them plainly, as I did above.
Pattern Two: Activating Inside a Module
Here is where refinements become genuinely elegant. If I call using inside a module, the refinement applies to everything the module defines, including nested classes and modules, and anything added to the module later, as long as the module is still open.
require_relative "text_helpers"
module Reporting
using TextHelpers
class Header
def initialize(raw)
@raw = raw
end
def to_s
@raw.squish.upcase
end
end
class Footer
def initialize(raw)
@raw = raw
end
def to_s
"-- #{@raw.squish} --"
end
end
end
Reporting::Header.new(" totals for june ").to_s
# => "TOTALS FOR JUNE"
Both classes inside the module see squish. Code outside the module does not. I use this pattern heavily when I am building a small subsystem—an import pipeline, a PDF builder, a report generator—where several classes share the same formatting needs but the rest of the app does not.
One caveat that has bitten me: this applies to modules you open with the module keyword, not to blocks passed to Class.new or Module.new. Those blocks have a different lexical scope and will not pick up the refinement. If you see a NoMethodError in a dynamically built class, that is usually the reason.
Pattern Three: Patching a Gem Without Touching the Gem
I try hard not to fork gems. Forking means maintenance, and maintenance means the fork drifts. Refinements let me correct one method for one caller without shipping a fork.
Say a gem’s client class returns a raw response and I need a different shape in my own adapter:
# lib/refinements/legacy_client_patch.rb
module LegacyClientPatch
refine LegacyClient::Response do
def body
JSON.parse(super, symbolize_names: true)
end
end
end
# app/adapters/billing_adapter.rb
require_relative "../../lib/refinements/legacy_client_patch"
using LegacyClientPatch
class BillingAdapter
def charge(amount)
response = LegacyClient::Response.new(fetch(amount))
response.body[:status] == "ok"
end
end
No other file gets the parsed body. The gem stays untouched. When the gem finally fixes the method upstream, I delete one file, and I can find every place that depended on my patch because using LegacyClientPatch is a single searchable string.
Compare that to reopening the class in an initializer. Even gem authors will tell you they prefer you refine. It keeps their support burden low and your upgrade path honest.
Pattern Four: Layering Two Refinements
Refinements can stack. If two refinements both change String#title and both are active, the one activated last sits on top, and it can call super to reach the one underneath.
module Format
refine String do
def title
split.map(&:capitalize).join(" ")
end
end
end
module Emphasis
refine String do
def title
"#{super}!"
end
end
end
Now watch the order matter:
module A
using Format
using Emphasis
def self.show(s) = s.title
end
module B
using Emphasis
using Format
def self.show(s) = s.title
end
A.show("hello wide world") # => "Hello Wide World!"
B.show("hello wide world") # => "Hello Wide World"
The super inside Emphasis#title always reaches the original class method, "hello wide world".title behavior defined on String itself. It does not reach the other refinement. That surprises people. Refinements do not chain into each other through super the way included modules do.
Where layering does help is in building a pipeline on one call site, like a formatter that first strips control characters and then normalizes whitespace, with each step in its own refinement. You keep each concern in a small file and activate them together in the file that owns the output.
Pattern Five: The Boundary Cases That Bite
This is the part nobody tells you, and it cost me real debugging hours.
Refinements are lexical. That means send and dynamic dispatch often do not see them. Walk through this slowly:
module Cleaner
refine String do
def squish = gsub(/\s+/, " ").strip
end
end
using Cleaner
" hi ".squish # => "hi"
" hi ".send(:squish) # NoMethodError
" hi ".respond_to?(:squish) # => false
Why? send digs into the object’s real method table, which never learned about squish. respond_to? asks the same table, so it also says no. The refinement lives in the lexical scope of the caller, not in the object, so anything that bypasses lexical lookup—send, method(:squish), public_send, instance_method—misses it.
Blocks and procs carry their own lexical scope with them. A proc written in a file with using Cleaner keeps the refined behavior wherever it is called. A proc written in a file without it never gains the behavior, even if you call it from a file that has the refinement active. That is the opposite of what most people assume.
There is a practical consequence for duck typing. If some code does if value.respond_to?(:squish), it will not detect your refined method. You have two honest options. First, do the work in a plain method on your own object and check for that method instead. Second, wrap the check in a module that also activates the refinement, so the check and the behavior share a lexical scope.
My advice is to treat these limits as features. Refinements are intentionally shy. Code that reflects on methods will not stumble into them, and that is exactly the isolation you wanted. Document the edges in a comment at the top of the refinement file, so the next person does not learn the hard way.
Pattern Six: Keeping Rails Behavior in One Room
Rails projects are where I get the most value from refinements, because Rails apps grow patches like weeds. Somebody changes Time#to_s(:db) in an initializer, and now every timestamp in the system is subtly different. Somebody patches the user model to add a helper the mailer needs, and suddenly every query on users goes through the patch.
Here is how I keep a patch local now:
# app/refinements/report_time.rb
module ReportTime
refine Time do
def stamp
strftime("%Y-%m-%d %H:%M")
end
end
end
# app/services/monthly_report.rb
require_relative "../refinements/report_time"
using ReportTime
class MonthlyReport
def generated_line
"Generated at #{Time.now.stamp}"
end
end
The method is available inside the service and nowhere else. A serializer, a job, or a controller that tries Time.now.stamp gets a clear NoMethodError, and no hidden behavior rides along. When you patch a gem’s internal class—especially one that shows up in authentication or money handling—this isolation is the difference between a scoped fix and an incident.
There is one Rails-specific rule I want you to tattoo somewhere. Do not activate the refinement in an initializer. Initializers run after the app boots, and autoloaded files often evaluate before that, so the class you think you are patching may have already been defined, and the refinement may arrive too late to apply. The same problem shows up in development because of code reloading.
I put refinements in app/refinements/, and I require the refinement file directly in the file that uses it. I never rely on the autoloader to bring the refinement in, because I want the using call and the file that needs the behavior to sit together in the same commit. That coupling is a good thing. It means the reader can see the whole story without opening another file.
Pattern Seven: Testing Refinements in Isolation
A refinement is a file-scoped affair, which sounds like it would make testing awkward. It does not. It makes testing better, because your test files become the only files that opt in, and the application files stay on stock behavior.
For RSpec, I put the using call at the top of the spec file:
# spec/refinements/text_helpers_spec.rb
require "rails_helper"
require_relative "../../app/refinements/text_helpers"
using TextHelpers
RSpec.describe TextHelpers do
it "collapses runs of whitespace" do
expect(" a b ".squish).to eq("a b")
end
it "does not leak out of this spec" do
isolated = " a b "
expect(isolated.respond_to?(:squish)).to be(false)
end
end
The second example matters as much as the first. It proves the isolation genuinely holds and that your test is not passing because of some leftover global patch. When I refactor a refinement later, both examples keep me honest.
Minitest works the same way. Put using inside the test class body:
require "minitest/autorun"
require_relative "../app/refinements/text_helpers"
class TextHelpersTest < Minitest::Test
using TextHelpers
def test_squish
assert_equal "a b", " a b ".squish
end
def test_does_not_leak
refute " a b ".respond_to?(:squish)
end
end
If your app code calls the refinement and you want an integration test that also covers the boundary, that is fine. Call the method that uses the refinement. Do not try to reach the refined method from the test file itself, because the test’s lexical scope is not the app file’s lexical scope. That is not a limitation. It is a reminder of where the behavior actually lives, and it will help you write a test at the right level.
What I Actually Do Day to Day
I keep refinements in their own directory and name the files after what they change, not where they are used. One concern per file. I put a comment at the top explaining why the refinement exists and what would let me delete it. When a gem fixes the issue, the file goes away and every call site becomes an error, which is a good thing because I want to know if I missed one.
I do not use refinements for cross-cutting concerns. Logging, authorization, and instrumentation belong in modules, decorators, or callbacks, where the call sites make them visible. Refinements are for the narrow case: one place needs a class to behave differently, and I do not want to change the world for it.
Performance is not a reason to avoid them. In modern Ruby, method lookup with refinements uses a global method cache, so calls from inside the refinement scope are fast. The cost moves to the moment you activate a refinement, especially early in a program, and to the size of your lookups. For everyday code this is invisible. Measure before you let it stop you.
The truth is that refinements have been stable since 2013, and many well-kept gems use them internally. They are one of the quietest features in Ruby, and that quietness is the point. A monkey patch is a shout heard across the whole application. A refinement is a sentence you say in one room, to one listener, and then forget, because everyone else is still having their original conversation.
Pick one place in your current project where a global patch makes you a little nervous. Wrap it in a refinement. Add the using line at the top of the one file that needs it, run the tests, and make sure the rest of the app never notices. That first taste of scoped change is hard to give up.