whoami
Lucas Jenß
cat /etc/motd
The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.
ls -la ~/languages/
- drwxr-xr-x
- ▶ PHP
- ▶ Ruby
- ▶ Scala
- ▶ C#
- ▶ JavaScript
- ▶ Objective-C
- ▶ Shell Scripting
ls -la ~/toolchain/
- drwxr-xr-x
- ▶ Typo3
- ▶ Akka
- ▶ Capistrano
- ▶ Git
- ▶ MAMP
- ▶ Adobe Illustrator
- ▶ NSTrackingArea (Cocoa)
uname -a
- drwxr-xr-x
- ▶ Mac OS X
- ▶ Unix
Replacing Ruby methods at runtime with Module#prepend
Ruby gives developers an unusual amount of flexibility when it comes to altering behaviour on the fly. Whether you are tracing a flaky spec in a Melbourne CI pipeline or patching a third-party gem for a Sydney fintech client, the ability to swap a method while the process is still running can save hours of debugging time.
This article walks through Module#prepend, the cleanest way to insert new behaviour above an existing method in the ancestors chain. You will learn how it differs from include and extend, how to call super correctly, and which pitfalls to watch out for when applying the technique to live Ruby applications in production.
Why runtime method swaps matter in Ruby
Most object-oriented languages lock a class definition at compile time. Ruby takes a different stance. Because classes are open and the method lookup path can be reordered while a process is running, you can replace almost any method without restarting the server.
This is invaluable in Australian engineering teams that deploy to platforms like AWS Sydney or Google Cloud's ap-southeast region. A hot patch that adds logging to a payment endpoint can be rolled out without a full redeploy, which matters when the team is on call during a Saturday AEST launch window. Hot patching also keeps test suites fast: you can mutate a single class for one example without redefining the original code across the project.
The same flexibility, however, makes it easy to write code that confuses the next reader. Three tools cover most of the cases: Module#prepend, alias_method chaining, and refinements. Each has a distinct effect on the ancestors chain and on how the original code is reached, so choosing the right one is half the battle.
How Module#prepend sits in the lookup chain
When you call a method on an object, Ruby walks up the ancestors array of the object's class. The class itself is checked first, then any modules that were included, in reverse order of inclusion. Module#prepend inserts the module ahead of the class in that list.
module Audit
def save
log_call
super
end
def log_call
Rails.logger.info("save called from #{caller_locations(1, 1).first}")
end
end
ActiveRecord::Base.prepend(Audit)
Now any save call on an ActiveRecord subclass first reaches Audit#save, runs the audit log, and then delegates to the original through super. The class itself never changes. Prepending the module gives Audit#save priority over the original without overwriting it, which is the key behaviour that separates it from a destructive redefinition.
This property makes prepend safer than the classic monkey patch of def String#blank? ... end. The original method remains intact and reachable, so a stray super call inside your replacement always finds the real implementation. Removing the prepend later is also trivial: a constant reassignment or removing the module reference from the class's prepended list restores the previous lookup order.
Building a first prepend example
Consider a scenario common in Australian e-commerce codebases: a legacy tax calculation that needs a temporary override while the team migrates to a new GST-inclusive pricing model after the ATO clarified a billing rule. Rather than editing the gem source, you can prepend a module.
module Gst2024Patch
def calculate_total
total = super
total * 1.10 # apply 10% GST temporarily
end
end
BillingService.prepend(Gst2024Patch)
The replacement runs inside the same BillingService instance, so callers do not need to change. Because the patched method calls super, the original tax rules still run — you only layer extra behaviour on top. Once the new pricing engine is live, removing the prepend line reverts the behaviour to its previous state.
This pattern is also handy for feature flags. You can prepend a module that raises NotImplementedError unless the feature flag is on, and Rails autoloading will pick it up the moment the flag flips in your environment. Sydney-based teams that use LaunchDarkly or local rollout toggles frequently lean on this trick during staged rollouts, especially when regulatory changes land on a known date and need a midnight AEST cutover.
Loading prepends at boot or on demand
The moment you call prepend shapes the rest of the patch's life cycle. Prepending during boot, inside an initializer or a Rails railtie, makes the change visible to every class loader and every worker thread from the start. Prepending later, inside a feature-flag callback or a Sidekiq job, applies only to whatever is running afterwards and creates a window where behaviour differs between requests.
Boot-time prepending is the safer default for production code. The Ruby VM has finished loading the host class, the ancestors chain is settled, and no thread is mid-call on the method. Australian teams running Rails on Passenger or Puma across multiple nodes in ap-southeast-2 should ship the prepend through their normal deployment pipeline so every node picks it up consistently, rather than relying on a runtime signal that may land on some workers first.
For experimental patches in development, on-demand prepending is fine and lets you toggle behaviour interactively in a console session. Wrap the call in a feature flag check so the prepend disappears in production unless an operator enables it. Keeping the prepend behind a flag also gives you a single rollback switch: flipping the flag off and restarting the workers is usually enough to undo the change.
Handling visibility and edge cases
Prepended modules inherit the visibility rules of the host class. If the original save is private, the prepended save is also private unless you redeclare it. Be explicit whenever you want a public replacement.
module PublicSave
def save
track_metrics
super
end
public :save
end
Another subtlety involves argument forwarding. When the original method accepts keyword arguments or a splat, your override must forward the same signature or risk subtle ArgumentError failures. Ruby 3.x tightened this behaviour further: a method that takes **opts will no longer auto-convert a hash, so use ... in your prepended method to forward everything unchanged.
Method redefinition warnings can also appear if the prepended module redefines an existing method on the class. Although prepend itself does not trigger them because it inserts a new ancestor, including a module that already has the method may still print a warning under -W. Suppress them in test environments with $VERBOSE = nil only when you genuinely cannot avoid the situation.
Prepend against alternatives
When you want to override behaviour, prepend is one of three common choices. A side-by-side comparison summarises how Module#prepend stacks up against the classic alias_method chain pattern and against refinements, the scoped counterpart introduced in Ruby 2.1.
| Approach | Scope | Reaches original via | Reversibility | Best fit |
|---|---|---|---|---|
| Module#prepend | Global, the whole class hierarchy | super | Easy, remove the prepend | Cross-cutting changes, gem patching |
| alias_method chain | Global, original renamed to _original_name | _original_name | Harder, requires restoring the alias | Permanent in-house patches |
| Refinements | Lexical, file or require scope | super inside using block | Automatic when the file finishes loading | Library authors, scoped fixes |
Refinements are appealing for library authors because the override disappears the moment the file finishes loading, so the host application is not affected. Alias chaining is older and tends to show up in long-lived code, but the renamed original pollutes the class with helper methods forever. Prepend hits a sweet spot for application code: it is global, reversible, and lets the original method stay clean.
Avoiding the foot-guns of method replacement
Three pitfalls trip up developers who reach for prepend too eagerly. The first is forgetting that prepend only affects instance methods. Class methods live on the singleton class, so overriding def self.x requires singleton_class.prepend or a separate prepend on the class's eigenclass.
The second pitfall is double prepending. If two libraries each prepend a module that overrides save, the second prepend wraps the first. Both super chains still run, but ordering becomes confusing. Track prepended modules in a constant on the host class so the order is obvious in code review and easy to reason about during an incident.
The third is thread safety. In a Puma cluster running across nodes in ap-southeast-2, every worker has its own ancestors chain, so prepending at boot is safe. Prepending while a request is mid-flight on another worker will not affect that worker, which is usually desirable. Avoid mid-request prepends in code that handles concurrent jobs from Sidekiq or GoodJob, where a single worker thread might re-enter the method and see partially-applied state.
If you are patching a gem that lives deep inside a C extension, keep in mind that prepend only works on Ruby methods. Native methods bound through FFI or the Ruby C API cannot be wrapped with super; alias the method first or replace the wrapper that calls into C. For an example of tracing that kind of C-level crash, the Android NDK crash trace walkthrough shows a similar boundary problem from the JVM side.
Start with a small, reversible prepend in a non-critical service. Pick a method that already has a spec, prepend a module that logs its arguments, and confirm super returns the expected result. Once you trust the pattern, move on to gem patches, feature flags, and audit logging in systems that have to stay warm through an Australian weekend deploy.
For a deeper dive into how I document these kinds of fixes, you can read more of my debugging notes over at ccjeng.com. The journal collects real production patches, including the Ruby metaprogramming experiments that grew out of projects for Sydney and Brisbane clients, and the lessons I learned when those patches had to come out again at 2am AEST.
cat ~/interests.json
| Key | Value |
|---|---|
| editor | Terminal-first workflow |
| os | Mac OS X / Unix |
| vcs | Git, distributed version control |
| deploy | Capistrano, cron automation |
| graphics | SVG, Adobe Illustrator troubleshooting |
| networking | IP validation, SSH, VPN |
git log --oneline --reverse
Solving SVG import issues in Adobe Illustrator CS6 and CC
When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.
Solving NDK build issues on OS X
Troubleshooting native development kit compilation problems on Mac OS X.
Programmatically adding PHP generated TypoScript to the backend configuration
Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.
ArgumentError: Could not parse PKey: no start line
Debugging an SSH key parsing error encountered during deployment.
Validating IP-Addresses in PHP
Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.
Cocoa: Using NSTrackingArea
A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.
cat ~/contact.txt