Sharing classes and services
Boxes isolate monkey patches to builtin classes: patching String in one Carton does not change String anywhere else. Classes a Carton defines are different. An exported class is one object with one definition, shared by every importer, as in ordinary Ruby. Reopening it from another Carton changes it for every Carton.
Box is not a security boundary, so this cannot be prevented outright. These habits avoid it by accident.
Subclass instead of reopening
Section titled “Subclass instead of reopening”Give each Carton its own base classes rather than binding a shared one to a bare name:
# customers/lib/customers.rb: the customers Carton's own base classclass ApplicationRecord < Framework::ApplicationRecord self.abstract_class = trueendChanges to a subclass never reach its parent.
Refine to extend another Carton’s class
Section titled “Refine to extend another Carton’s class”A refinement applies only to files that activate it with using, so the owning Carton never sees it.
Freeze plain-Ruby exports
Section titled “Freeze plain-Ruby exports”export Pricing.freeze makes any reopen raise FrozenError while still allowing subclasses. Do not freeze Active Record models or their base classes: Rails writes class-level state at runtime, so even create! raises once a model’s class is frozen.
Passing application services
Section titled “Passing application services”A Carton cannot see the application’s constants, but it can call objects the application passes in. Use ordinary constructor arguments to supply a repository, payment client, or logger:
# main.rb: the application owns the services and passes them inCheckout = import_relative 'checkout'checkout = Checkout::Service.new(orders:, payments:, logger:)checkout.call(order_id: 42)An injected object’s methods run in the Box where they were defined, with that Box’s constants and gems. The Carton needs only the methods it calls, so tests can pass fakes through the same interface. Objects are shared, not copied; keep per-instance dependencies on instances instead of the shared exported module. Prefer plain values for inputs and results when each side loads its own gem classes.
The dependency_injection example runs this checkout with file-backed adapters and tests it through its export with fakes.
