Ruby 4.0 introduces Ruby::Box, a feature that fundamentally changes how we think about code isolation. Imagine running two different versions of the same library simultaneously, or safely monkey-patching String without affecting your entire application.
That’s Ruby::Box.
box = Ruby::Box.new
box.require('my_library')
box::MyClass.new #  Exists in the box
MyClass.new #  NameError — doesn't exist outside!
Each box is a parallel universe with its own classes, constants, and global variables — completely isolated from everything else.
The Problem: Ruby’s Beautiful Mess
Ruby’s open classes are a double-edged sword. The same flexibility that makes Ruby delightful also creates nightmares:
# Two gems. Same method. Who wins?
# gem_a
class String
def format = "A: #{self}"
end
# gem_b
class String
def format = "B: #{self}"
end
"hello".format #  It's a gamble.
Or consider migrating between API versions:
# You need BOTH versions during migration
PaymentAPI::V1.charge(100) # Old way
PaymentAPI::V2.process(Money.new(100)) # New way
# But loading both? Constant collision. 
Ruby::Box eliminates these conflicts entirely.
Getting Started
Enable Ruby::Box with an environment variable (it’s experimental in 4.0):
RUBY_BOX=1 ruby my_script.rb
Then create isolated worlds:
# Check if enabled
Ruby::Box.enabled? # => true
# Create a box
box = Ruby::Box.new
# Load code into it
box.require('my_library')
box.require_relative('./local_file')
# Access what's inside
instance = box::MyClass.new
The Hierarchy
┌─────────────────────────────┐
│ ROOT BOX │ ← Built-in classes (String, Array, etc.)
└─────────────────────────────┘
│
┌────┴────┐
▼ ▼
┌────────┐ ┌────────┐
│ MAIN │ │ USER │ ← Your code lives here
│ BOX │ │ BOXES │
└────────┘ └────────┘
Every Ruby::Box.new creates a fresh copy from the root. Boxes are siblings, not nested.
What Gets Isolated?
| Isolated | Shared |
|---|---|
| Class/module definitions | Object instances |
| Constants | Built-in class identity |
Global variables ($foo) |
Core method implementations |
| Monkey patches | — |
Class variables (@@var) |
— |
The magic: String and box::String are the same class, but each box can have different methods added to it.
The Killer Use Case: Running Multiple Versions
This is where Ruby::Box shines. Run two library versions side by side during a migration:
# api_v1.rb
module PaymentAPI
VERSION = "1.0"
def self.charge(amount)
{ status: 'charged', amount: amount }
end
end
# api_v2.rb
module PaymentAPI
VERSION = "2.0"
def self.charge(amount, currency: 'USD')
{ status: 'processed', amount: amount, currency: currency }
end
end
# Load each version in its own box
v1 = Ruby::Box.new
v2 = Ruby::Box.new
v1.require_relative('api_v1')
v2.require_relative('api_v2')
# Use both simultaneously!
v1::PaymentAPI.charge(100)
# => { status: 'charged', amount: 100 }
v2::PaymentAPI.charge(100, currency: 'EUR')
# => { status: 'processed', amount: 100, currency: 'EUR' }
# Compare during migration
puts "V1: #{v1::PaymentAPI::VERSION}" # => "1.0"
puts "V2: #{v2::PaymentAPI::VERSION}" # => "2.0"
No conflicts. No hacks. Just clean separation.
Safe Monkey Patching
Extend core classes without polluting the global namespace:
# extensions.rb
class String
def shout = upcase + "!"
def whisper = downcase + "..."
end
box = Ruby::Box.new
box.require_relative('extensions')
# Inside the box
box.eval('"hello".shout') # => "HELLO!"
# Outside the box
"hello".shout # => NoMethodError 
Your extensions stay contained. No surprises for other code.
Building a Plugin System
Ruby::Box makes plugin architectures trivial:
class PluginManager
def initialize
@plugins = {}
end
def load(name, path)
box = Ruby::Box.new
box.require(path)
@plugins[name] = { box: box, instance: box::Plugin.new }
end
def run(name, method, *args)
@plugins[name][:instance].send(method, *args)
end
end
# Each plugin can define String#transform differently
manager = PluginManager.new
manager.load(:markdown, 'plugins/markdown')
manager.load(:sanitize, 'plugins/sanitize')
manager.run(:markdown, :transform, "**bold**") # => "<strong>bold</strong>"
manager.run(:sanitize, :transform, "<script>") # => "<script>"
Plugins are completely isolated. They can’t step on each other’s toes.
Best Practices
1. Name your boxes — Makes debugging easier:
class NamedBox
attr_reader :name
def initialize(name)
@name = name
@box = Ruby::Box.new
end
def inspect = "#<Box:#{@name}>"
def method_missing(m, *a, &b) = @box.send(m, *a, &b)
end
end
payments = NamedBox.new(:payments)
2. Centralize box management:
module Boxes
def self.payments
@payments ||= begin
box = Ruby::Box.new
box.require('lib/payments')
box
end
end
Boxes.payments::Processor.charge(100)
3. Cache class references in hot paths:
# Slower
items.map { |i| @box::Processor.new.process(i) }
# Faster — cache the class
processor_class = @box::Processor
items.map { |i| processor_class.new.process(i) }
Limitations to Know
Ruby::Box is experimental in 4.0. Some things to watch:
| Issue | Workaround |
|---|---|
| Native extensions may fail during install | Install gems without RUBY_BOX=1, then enable it |
| ActiveSupport core_ext issues | Load ActiveSupport in the main box only |
| C-implemented methods can’t be overridden per-box | Only Ruby-defined methods are isolated |
# This won't work — String#length is C code
box.eval('class String; def length = 42; end')
"hello".length # => 5, not 42
Debugging Tips
# Where am I?
Ruby::Box.current
# Am I in root?
Ruby::Box.current == Ruby::Box.root
# What's defined here?
box.constants # => [:MyClass, :MyModule, ...]
The Bottom Line
Ruby::Box solves problems that have plagued Ruby developers for years:
- Library conflicts → Each library gets its own box
- Version migrations → Run old and new simultaneously
- Monkey patch pollution → Extensions stay contained
- Plugin isolation → Plugins can’t break each other
- Multi-tenant customizations → Per-tenant code without interference
It’s experimental now, but this is the future of Ruby code organization.
Start Experimenting
# Try it today
RUBY_BOX=1 irb
box = Ruby::Box.new
box.eval('X = 42')
box::X # => 42
X # => NameError — isolated!
Ruby::Box brings the isolation guarantees of separate processes with the performance of a single process. As it matures, expect it to become an essential tool in every Rubyist’s toolkit.
The Ruby core team has delivered something special with 4.0. Happy holidays and happy coding! 
Further Reading:
Comments (0)