The letter A styled as Alchemists logo. lchemists
Published September 10, 2021 Updated May 13, 2026
Cover
Ruby Zeitwerk

Zeitwerk is a Ruby gem for auto-loading/reloading of objects within your project and is a core part of frameworks like Hanami and Ruby on Rails. To quote directly from Zeitwerk’s documentation:

Given a conventional file structure, Zeitwerk is able to load your project’s classes and modules on demand (autoloading), or upfront (eager loading). You don’t need to write require calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby’s semantics for constants.

With the above in mind, I want to focus on using Zeitwerk within pure Ruby projects and/or gems and how tools like Rubysmith or Gemsmith can speed up this process for you. ⚡️

Directories

When working with Ruby projects and gems, there are two directory structures to keep in mind which depend on the project or gem name chosen.

Basic

Basic structures consist of either a standard or underscored project name. For example, if you name your project demo, you’d use the following structure:

demo
├── lib
│  └── demo.rb

…​which would result in the following implementation of demo.rb:

require "zeitwerk"

Zeitwerk::Loader.new.then do |loader|
  loader.tag = File.basename __FILE__, ".rb"
  loader.push_dir __dir__
  loader.setup
end

# Main namespace.
module Demo
end

Same goes underscored project names like, for example, demo_test:

demo_test
├── lib
│  └── demo_test.rb

…​which would result in the following implementation of demo_test.rb:

require "zeitwerk"

Zeitwerk::Loader.new.then do |loader|
  loader.tag = File.basename __FILE__, ".rb"
  loader.push_dir __dir__
  loader.setup
end

# Main namespace.
module DemoTest
end

Notice that in both of the above cases, each project is properly titleized/camelcased appropriately:

  • Titleize: demo → Demo

  • Camelcase: demo_test → DemoTest

You might also be wondering what each line of the above Zeitwerk code blocks are doing so here’s a line-by-line breakdown:

# Sets tag to "demo" or "demo_test".
loader.tag = File.basename __FILE__, ".rb"

# Ensures current directory (i.e. ".") is registered.
loader.push_dir __dir__

# Ensures the loader is properly configured based on the above settings.
loader.setup

You might also be wondering why I haven’t talked about Zeitwerk::Loader.for_gem which reduces the above code blocks to a single line. The problem with using Zeitwerk::Loader.for_gem is it only works with simple gem structures — like Demo in the first example — and uses the following configuration:

  • Uses Zeitwerk::GemInflector instead of the default Zeitwerk::Inflector.

  • Configures Zeitwerk to look for a lib/demo/version.rb file. This is great if you need that file but no project created with Rubysmith or Gemsmith makes use of that file since it’s an unnecessary maintenance burden.

So far I’ve been talking about Ruby projects and gems interchangeably. The reason is that no matter if you are building a pure, stand-alone Ruby project or a Ruby gem, the structure is the same.

Nested

With nested project structures, there is a slight caveat because behavior is altered when a project name use dashes. Instead, we get a nested directory and object namespace. For example, if you name your project demo-test, you’ll end up with the following structure:

demo-test
├── lib
│  └── demo
│     └── test.rb

…​which results in the following implementation of demo/test.rb:

require "zeitwerk"

Zeitwerk::Loader.new.then do |loader|
  loader.tag = "demo-test"
  loader.push_dir "#{__dir__}/.."
  loader.setup
end

module Demo
  # Main namespace.
  module Test
  end
end

Notice — due to the dash in the project name — we have Test nested within the Demo namespace. Additionally, the corresponding file is structured as demo/test.rb. Finally, we have to teach Zeitwerk to load the project one directory up from where where Zeitwerk is initialized in order to load the entire library. This includes providing a specific tag since we can’t use the base name of the current file due to being partially incomplete (i.e. "test" instead of "demo-test").

This is a conventional and standard practice for organizing Ruby projects and gems. You can find this enforced via the following gems:

  • Rubysmith: Focused specifically on building Ruby projects only. Example usage: rubysmith build --name demo.

  • Gemsmith: Built for professional gem smithing and a step above what you get with Bundler. Example usage: gemsmith build --name demo.

  • Bundler: Default with all Ruby installations which can be handy for quick and dirty building of gems. Example usage: bundle gem demo.

All three of the gems above share the same behavior as Zeitwerk when it comes to conventional structures for Ruby projects.

Namespaces

In addition to directory structures, Zeitwerk will generate missing module ancestry. Consider the following:

# Nested - What you want to to do.
module One
  module Two
    class Demo
    end
  end
end

# Flat - What you want to avoid.
class One::Two::Demo
end

While the flat definition is an antipattern, Zeitwerk will workaround this implementation flaw by ensuring modules One and Two are dynamically created for you. Otherwise, you’d be fighting with constant resolution errors. That said — and even though Zeitwerk will resolve this for you — avoid writing code like this because in situations where you end up not using Zeitwerk or removing Zeitwerk entirely, you’ll have to contend with this by resolving all conflicts manually.

Constant Reload and Reference

When using Ruby’s standard require, any code changes applied after the code was required and loaded, would not be picked up. With Zeitwerk, all of this is handled for you so you can have some of the same code reloading functionality as found in web frameworks.

In addition to constant reloading, Zeitwerk also makes it possible to reference auto-loaded constants without requiring them first. This is especially handy in projects like ROM or Hanami.

Dependencies

Loading gem dependencies is not within the realm of what Zeitwerk manages for you even if Zeitwerk is used as a loader within your dependencies. The standard practice is to require your dependencies via your Gemfile or Gem Specification and then manually require each dependency within each file of your implementation where the dependency is used. Example:

require "http"

class Pinger
  def initialize http: HTTP
    @http = http
  end

  def get(path) = http.get path

  private

  attr_reader :http
end

With the above, we see the HTTP gem is required via require "http" which is good practice due to the explicit nature of requiring your dependency as needed. That said, you can use native autoload functionality to load your dependencies for use anywhere in your gem, project, or application. Example:

autoload :HTTP, "http"
autoload :JSON, "json"

The above works great for centralizing the loading of dependencies as necessary at the cost of forcing upstream users of your implementation to load your entry point file where all of this is defined. This situation doesn’t happen when manually requiring what you need.

Both approaches have tradeoffs but either can be valuable when used wisely.

Rubysmith

Now that you understand directory structures, namespacing, and constant resolution, we can talk about how Rubysmith can automate your workflow further when building new projects. The great news is Rubysmith has Zeitwerk support built in by default. 🎉 This means you can use Rubysmith to build any of the following example projects:

rubysmith --build demo
rubysmith --build demo_test
rubysmith --build demo-test

You’ll get identical structures, as talked about earlier, each complete with Zeitwerk support by default. In situations you don’t desire Zeitwerk support, you can disable Zeitwerk when building a project. Example:

rubysmith --build demo --no-zeitwerk

…​which yields the following implementation within the lib/demo.rb file:

# Main namespace.
module Demo
end

Without Zeitwerk support, you’ll have to manually add require statements as you build out more of our implementation. There are definitely use cases where you want this behavior, especially when working on low-level gems where you want few dependencies. For the most part, letting Zeitwerk do the heavy lifting is a nice win and Rubysmith/Gemsmith has you covered in that regard!

Gemsmith

Gemsmith is built atop Rubysmith but is specialized for building Ruby gems. Usage is the similar to the above, though:

# With Zeitwerk
gemsmith --build demo
gemsmith --build demo_test
gemsmith --build demo-test

# Without Zeitwerk
gemsmith --build demo --no-zeitwerk

Rails

If working in a Rails application, you can leverage the above by having Zeitwerk manage embedded gems within the gems folder of your application. This provides two major benefits:

  1. A strong separation of concerns so your embedded gems have no knowledge of Rails.

  2. Allows you to extract the gem should the need arise without having to prematurely manage multiple projects.

The steps for making this happen are simple. First, configure your Rails application to ignore the gems folder in the root of your application:

# config/application.rb
$LOAD_PATH.push Rails.root.join("gems")

You could use your existing lib folder but having a distinct gems folder improves organization without intermingling existing lib functionality. Next, setup Zeitwerk to load and manage your embedded gem:

# your_rails_app/gems/demo.rb
require "zeitwerk"

Zeitwerk::Loader.new.then do |loader|
  loader.tag = "demo"
  loader.push_dir __dir__
  loader.setup
end

# The gem namespace.
module Demo
end

Finally, update your RSpec helper to load your gem:

# spec/spec_helper.rb
require_relative "../gems/demo"

RSpec.configure do |config|
  # Your configuration details go here.
end

The above allows you test everything at the Ruby level while inheriting this setup via your rails_helper.rb so all of this is picked up and run as part of your entire test suite.

💡 You can use Gemsmith to automate this process, without manually creating your Zeitwerk configuration, by following these steps:

  1. cd $HOME/Scratch

  2. gemsmith build --name demo --min --readme --zeitwerk

  3. mv $HOME/Scratch <your_rails_app>/gems

  4. cd <your_rails_app>

  5. mv gems/demo/spec/lib/demo_spec.rb spec/gems

Debugging

When it comes to debugging the Zeitwerk loader for your project, both Rubysmith and Gemsmith ensure a loader is provided for you. For example, using the same Demo project as shown above, you’d have the following available to you:

require "zeitwerk"

Zeitwerk::Loader.new.then do |loader|
  loader.tag = File.basename __FILE__, ".rb"
  loader.push_dir __dir__
  loader.setup
end

# Main namespace.
module Demo
  def self.loader registry = Zeitwerk::Registry
    @loader ||= registry.loaders.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  end

This means you could generate a new Ruby project, like so:

rubysmith --build demo
cd demo
bin/console

Then, within the console, you could use Demo.loader to inspect your Zeitwerk loader specific to your project. Here’s a screenshot showing all the steps and full output:

Cover

Not only can you look at your own project’s loader but you can also inspect all other loaders in use by using the Zeitwerk registry to answer an array of loaders. Example:

Zeitwerk::Registry.loaders

This information is handy when debugging your loaders and logging is enabled (i.e. loader.log!) due to the loader tags configured in the earlier examples.

Conclusion

I hope you’ve enjoyed this brief exploration of the Zeitwerk gem for use in your own Ruby projects. Consider giving Rubysmith a try the next time you are building a Ruby project. You might be pleasantly surprised in the productivity boost you gain from having a tool like this in your back pocket.