Skip to content

Getting started

In plain Ruby, every file can reach every constant and every monkey patch applies everywhere. Carton splits an app into gem-sized or domain-sized Cartons, and each Carton is a boundary. It runs in its own Ruby::Box, so its constants, gems, and monkey patches stay inside, and only what it exports crosses. Why Carton covers the problems this solves.

You’ll build two Cartons, then watch Ruby stop the app from reaching past one into the other.

  • Ruby 4.0.7 or later, run with Boxes enabled: RUBY_BOX=1
  • Bundler 4.0.20 or later, if you use Bundler
  • require 'carton' before the app first calls import, import_relative, or export; every Carton Box gets Carton automatically

Build a two-Carton app, then check the boundary.

  1. gem install carton

    Or add it to your Gemfile:

    Gemfile
    gem 'carton'
  2. A Carton’s Entrypoint is a Ruby file that exports one value, usually its root module. Start with a tax Carton:

    # tax.rb: the tax Carton
    module Tax
    def self.on(total) = total / 10
    end
    export Tax # the only value that leaves this Carton
  3. Cartons import each other. The billing Carton imports tax:

    # billing.rb: the billing Carton
    Tax = import_relative 'tax' # billing's import, invisible to Importers
    module Billing
    def self.invoice(total) = total + Tax.on(total)
    end
    export Billing

    The application imports billing from the Main Box:

    main.rb
    require 'carton'
    Billing = import_relative 'billing' # runs billing.rb in its own Box
    puts Billing.invoice(100)

    import_relative returns the exported value itself, and defines nothing in the Importer. Bind it to a constant of the same name.

  4. Run it with Boxes enabled:

    RUBY_BOX=1 ruby main.rb # prints 110
  5. Add a line to main.rb that reaches past billing:

    Tax # NameError: billing's import

    In plain Ruby, a file’s top-level constants are global, so requiring billing would make Tax visible everywhere. The billing Carton keeps its imports inside, along with every other top-level constant and method it defines.

    Monkey patches stay inside too. Patch String at the top of billing.rb:

    billing.rb
    class String
    def blank? = strip.empty? # billing's patch
    end

    Billing’s code can call ''.blank?, but main.rb can’t:

    ''.blank? # NoMethodError

When only billing uses tax, make it part of billing. A Carton is its Entrypoint plus its Interior, the directory named after it, shaped like a gem’s lib/. Move tax.rb to billing/tax.rb, nest Tax under Billing as a gem would, and drop its export:

billing.rb # the Entrypoint
billing/tax.rb # the Interior: inside the billing Carton
billing/tax.rb
module Billing
module Tax
def self.on(total) = total / 10
end
private_constant :Tax # hidden from Importers of Billing
end

Load it from the Entrypoint with require_relative:

billing.rb
require_relative 'billing/tax' # loads inside billing's Box
module Billing
def self.invoice(total) = total + Tax.on(total)
end
export Billing

main.rb still prints 110. Tax is still a NameError there, and Billing::Tax raises NameError because it is private. Only the Entrypoint can be imported, so import_relative 'billing/tax' raises Carton::ImportError.

Each Carton loads once per process, and every later import returns the same value. import 'billing' searches $LOAD_PATH like require, so gems and path gems import by name.