Code Loaders in Ruby: Understanding Zeitwerk

What makes Rails magical? It just might be its code loader. Put a few files in the right places, and - presto! - you have a web app. When you use a class, Rails handles the include so you can stay focused on your code. But this magic isn't just for Rails! You can add thread-safe code loading to your own apps via the Zeitwerk gem. In this article, Olasubomi introduces us to Zeitwerk and shows us how to integrate it with our own projects.

Code Loaders in Ruby - Understanding Zeitwerk

With Zeitwerk, you can streamline your programming knowing that classes and modules are available everywhere.

What are Code Loaders?

Code loaders let developers define classes and modules across different files and folders and use them throughout the codebase without explicitly requiring them. Rails is a good example of a piece of software that uses code loaders. Programming in Rails doesn't require explicit require calls to load models before using them in controllers. In fact, in Rails 6, everything in the app directory is auto-loaded on app boot, with a few exceptions.

While it is easy to think code loading is all about making calls to require, it isn't that simple. Code loading can be further broken down into three parts, as follows.

  • Auto Loading: This means code is loaded on-the-fly as required. For example, in Rails, running rails s doesn't load all the models, controllers, etc. But, on the first hit of the model User, it runs the auto loading mechanism to find and use the model. This is auto loading in action. This has some advantages for our development environment, as we have faster app and rails console startup times. Rails.config.autoload_path controls the paths to be auto-loaded.
  • Eager Loading: This means code is loaded into memory at app startup and doesn't wait for the constants to be called before requiring it. In Rails, code is eager loaded in production. From the explanation above, autoloading code in production will result in slow response times, as each constant will be required on-the-fly. Rails.config.eager_load_paths controls the paths to be eager loaded.
  • Reloading: The code loader is constantly watching for changes to files in the autoload_path and reloads files when it notices any changes. In Rails, this can be quite useful in development, as it enables us to run rails s and simultaneously make changes without needing to restart the rails server. This is reloading in action.

We can easily see that most of these concepts have been developed and live in Rails. Zeitwerk changes this! Zeitwerk enables us to bring all the code loading action to any Ruby project.

What is Zeitwerk?

Zeitwerk is an efficient and thread-safe code loader for Ruby and can be used in any Ruby project, including Web frameworks (Rails, Hanami, Sinatra), Cli tools, and gems. With it, you can streamline your programming knowing that classes and modules are available everywhere. Traditionally, Rails and some other gems have built-in code loaders to enable this functionality. However, Zeitwerk extracts these concepts into a gem and allows Rubyists to apply these concepts to their projects.

Installing Zeitwerk

First things first, we need to install the gem:

gem install zeitwerk

# OR in your Gemfile
gem 'zeitwerk', '~> 2.4.0'

Configuring Zeitwerk

So let's start with the basics:

require 'zeitwerk'
loader = Zeitwerk::Loader.new
...
loader.setup

The above code instantiates a loader instance and calls setup. After the call to setup, loaders are ready to load code. But, before that, all the necessary configurations on the loader should be covered already. In this article, I'll cover a few of the configurations on the loader and conventions for structuring your code.

  • File Structure: For Zeitwerk to work, files and directory names need to match the modules and class names they define. For example,
  lib/my_gem.rb         -> MyGem
  lib/my_gem/foo.rb     -> MyGem::Foo
  lib/my_gem/bar_baz.rb -> MyGem::BarBaz
  lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
  • Root Namespaces: Root Namespaces are directories where Zeitwerk can find your code. When modules and classes are referenced, Zeitwerk knows to search the root namespaces with the matching file name. For example,
  require 'zeitwerk'
  loader = Zeitwerk::Loader.new
  loader.push_dir("app/models")
  loader.push_dir("app/controllers")

  // matches as follows
  app/models/user.rb                        -> User
  app/controllers/admin/users_controller.rb -> Admin::UsersController

There are two primary ways to define the root namespace for two different use cases. The default way is shown below:

  // init.rb
  require 'zeitwerk'
  loader = Zeitwerk::Loader.new
  loader.push_dir("#{__dir__}/bar")
  ...
  loader.setup

  // bar/foo.rb
  class Foo; end

This means the class Foo can be referenced without an explicit Bar::Foo, as the bar directory acts as a root namespace. The second way to define a namespace is to explicitly state the namespace in the call to push_dir:

  // init.rb
  require 'zeitwerk'

  module Bar
  end
  loader = Zeitwerk::Loader.new
  loader.push_dir("#{__dir__}/src", namespace: Bar)
  loader.setup

  // src/foo.rb
  class Bar::Foo; end

There are a few things to note from in this code:

  1. The module Bar was already defined before being used by push_dir . If the module we want to use is defined by a third-party, then a simple require will define it before we use it in our call to push_dir.
  2. The push_dir explicitly specifies the namespace Bar.
  3. The file src/foo.rb defined Bar::Foo, not Foo, and did not need to create the directory, like src/bar/foo.rb.
  • Independent code loader: By design, Zeitwerk allows each project or app dependency to manage its individual project tree. This means the code loading mechanism of each dependency is managed by that dependency. For example, in Rails 6, Zeitwerk handles code loading for the Rails app and allows each gem dependency to manage its own project tree separately. It is an error condition to have overlapping files between multiple code loaders.

  • Autoloading: With the above setup, once the call to setup is made, all classes and modules will be available on demand.

  • Reloading: To enable reloading, the loader has to be explicitly configured for it. For example,

  loader = Zeitwerk::Loader.new
  ...
  loader.enable_reloading # you need to opt-in before setup
  loader.setup
  ...
  loader.reload

The loader.reload call reloads the project tree on-the-fly, and any new changes are visible immediately. However, we still need a surrounding mechanism to detect changes to the file system and call loader.reload. A simple version is shown below:

  require 'filewatcher'

  loader = Zeitwerk::Loader.new
  ...
  loader.enable_reloading
  loader.setup
  ...

  my_filewatcher = Filewatcher.new('lib/')
  Thread.new(my_filewatcher) {|fw| fw.watch {|filename| loader.reload } }

Using Zeitwerk in Rails

Zeitwerk is enabled by default in Rails 6.0. However, you can opt-out of it and use the Rails classic code loader.

# config/application.rb
config.load_defaults "6.0"
config.autoloader = :classic

Using Zeitwerk in Gems

Zeitwerk provides a convenient method for gems, as long as they use the standard gem structure (lib/special_gem). This convenience method can be used as follows:

# lib/special_gem.rb
require 'zeitwerk'

module SpecialGem
end

loader = Zeitwerk::Loader.for_gem
loader.setup

With the standard gem structure, the for_gem call adds the lib directory as a root namespace, enabling every code in the lib directory to be found automatically.

For more inspiration, you can check out gems using Zeitwerk:

References

Rails autoloading — how it works, and when it doesn't

Zeitwerk

Honeybadger has your back when it counts. We're the only error tracker that combines exception monitoring, uptime monitoring, and cron monitoring into a single, simple to use platform.

Our mission: to tame production and make you a better, more productive developer. Learn more

author photo

Subomi Oluwalana

Subomi is a passionate software engineer. His main focuses are Ruby and backend development.


“We’ve looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.”
Michael Smith
Try Error Monitoring Free for 15 Days
Are you using Bugsnag, Rollbar, or Airbrake for your monitoring? Honeybadger includes exception, uptime, and check-in monitoring — all for probably less than you’re paying now. Discover why so many companies are switching to Honeybadger here.
Try Error Monitoring Free for 15 Days
Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
Try Error Monitoring Free for 15 Days