In Ruby, when two gems both reopen Integer to define minutes, which definition wins, and how would you notice the collision?
answer
- one method table, one entry
- require order decides
- no exception on redefinition
- ruby -w: method redefined; discarding old
- a spec that pins the patched result
basics
~20 sThe definition loaded last wins, for every caller in the process: reopening Integer overwrites the single minutes entry in Integer's method table. Ruby raises nothing; only with warnings on (ruby -w) does it print 'method redefined; discarding old minutes'.
solid answer
~40 s`class Integer ... end` does not create anything new — it reopens the one existing `Integer` class, and `def minutes` writes into that class's method table. When a second gem does the same, its `def` replaces the first entry, so whichever file is required later decides what `5.minutes` means everywhere, including inside the first gem's own code. Redefinition is legal Ruby, so nothing is raised. With `$VERBOSE` true (`ruby -w`) Ruby prints `method redefined; discarding old minutes` plus the file and line of the previous definition, which is the quickest way to spot it; a spec asserting what the app expects `2.minutes` to return catches the day a dependency upgrade flips the load order.
code
ruby · 15 lines# gem_a/core_ext.rb
class Integer
def minutes = self * 60
end
# gem_b/core_ext.rb (required later)
class Integer
def minutes = "#{self} minutes"
end
2.minutes # => "2 minutes" -- gem_a's own callers get this too
# $ ruby -w app.rb
# gem_b/core_ext.rb:2: warning: method redefined; discarding old minutes
# gem_a/core_ext.rb:2: warning: previous definition of minutes was herego deeper
Remember that reopening a class changes the one shared class, so the last definition loaded is the one every caller gets.
Explain the method table being overwritten, why nothing is raised, and that ruby -w prints the 'method redefined' warning with both locations.
Show how you would find the two definers, why reordering requires only moves the breakage, and how a pinning test catches it after upgrades.
Frame core patches in shared dependencies as a supply-chain risk: decide which libraries may patch core classes and require opt-in core_ext files.
## Open classes: one class, one method table In Ruby, the `class` keyword first looks up the constant it names. If `Integer` already exists and is a class, the body runs **inside that existing class** with `self` set to it — this is called *reopening* the class, and a patch made this way is a **monkey patch**. Every `def` in the body writes an entry into `Integer`'s own **method table**, the per-class map from method name to method body. There is exactly one `Integer` class per process. Gems, the application and the standard library all share it, and method lookup happens at call time, so a method added now is visible to every integer, including ones created before the patch ran. ## What happens when a second gem defines the same name Suppose `gem_a` defines `Integer#minutes` returning seconds (`self * 60`), and `gem_b` defines `Integer#minutes` returning a duration object. Both reopen `Integer`. | Step | Effect on `Integer`'s method table | |---|---| | `require "gem_a"` runs `def minutes` | adds the entry `minutes -> gem_a's body` | | `require "gem_b"` runs `def minutes` | **replaces** that entry with gem_b's body | | any code calls `5.minutes` | finds gem_b's body, including code inside gem_a | The consequences: 1. **Last loaded wins.** Load order, not intent, decides the behaviour, and load order can change when a Gemfile is reordered or a dependency starts requiring the other gem earlier. 2. **The loser breaks far from the cause.** gem_a's own code now receives a duration where it expected an integer, and fails with an unrelated error such as a `NoMethodError` or a `TypeError` somewhere inside gem_a. 3. **Nothing stops it.** Redefining a method is ordinary Ruby — the same mechanism that lets you patch a bug in a dependency — so the interpreter raises no exception. ## Noticing the collision Ruby does tell you, but only when asked: - **Verbose mode.** With `$VERBOSE` true — `ruby -w`, or `RUBYOPT=-w` for a test run — redefining a method defined in Ruby code prints `method redefined; discarding old minutes` followed by `warning: previous definition of minutes was here` with the earlier file and line. Without `-w`, nothing is printed. - **An exception to the warning.** If the old method had been **aliased** first (the classic alias-chain patch), Ruby skips the warning, because keeping the old body under another name is the point of an alias chain. - **A pinning test.** A test that asserts the behaviour the application relies on (`assert_equal 120, 2.minutes`, or the RSpec equivalent) fails the day an upgrade changes which definition wins. This catches the collision even when warnings are off. - **Reading the load path.** When a warning names two files, those are the two definers; the fix is a decision between them, not a reordering. ## Resolving it Reordering `require` lines only chooses a different victim. Real fixes, roughly in order of preference: - **Stop one gem from patching.** If a gem ships its core patches as a separate, opt-in `core_ext` file, do not require that file; otherwise ask its maintainers for that split or replace the gem. - **Wrap instead of patch** in application code: a helper such as `Duration.minutes(5)` does not touch `Integer` at all. - **Scope the patch** with a refinement so it applies only in files that activate it (refinements are their own topic). - **Guard, carefully.** `unless Integer.method_defined?(:minutes)` stops the overwrite but hides the conflict: the first definer now wins, and the second gem silently runs with semantics it did not write. ## Why interviewers ask this The question checks whether a candidate understands that a Ruby class is a single mutable object shared by the whole process, not a per-file declaration. A strong answer names the load-order rule, the verbose warning and a test that pins the behaviour, and treats "it worked on my machine" as a symptom of require order.
- Does wrapping the second patch in `unless Integer.method_defined?(:minutes)` fix the collision?It stops the overwrite, so now the first gem loaded wins instead of the last. Both gems still assume their own meaning of `minutes`, so the second gem runs against a method it did not write. The guard hides the conflict rather than resolving it; raising when the method already exists at least fails loudly at boot.
- Why doesn't Ruby raise an error when a method is redefined?Open classes are a deliberate language feature: patching a dependency's bug, code reloading and alias chains all rely on replacing a method in place. Ruby therefore treats redefinition as legal and reports it only as a verbose-mode warning, `method redefined; discarding old ...`.
- Why can the collision appear only after a dependency upgrade?Which definition wins depends on the order files are required. An upgrade can make one gem require the other earlier, or add a new gem that patches the same method, so the last definer changes even though no application code changed.
saying these in an interview costs you the question
- Ruby raises an error when a second gem redefines an existing method.
- The first definition is kept; later definitions of the same method are ignored.
- Each gem keeps seeing its own version of Integer#minutes.
- Reopening Integer creates a new class that shadows the built-in one.
- The redefinition warning is printed by default, even without -w.