Skip to content

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.

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 class
class ApplicationRecord < Framework::ApplicationRecord
self.abstract_class = true
end

Changes to a subclass never reach its parent.

A refinement applies only to files that activate it with using, so the owning Carton never sees it.

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.

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 in
Checkout = 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.