Autoloading, Zeitwerk and boot

Ruby on Rails · Expert · 14 min

Autoloading errors and boot-time mistakes are confusing until you know the rules Zeitwerk follows.

Rails apps never require their own files. Zeitwerk maps file paths under autoload directories (app/* subdirectories, lib via config.autoload_lib) to constants by convention: app/models/user_profile.rb must define UserProfile, and app/services/billing/invoice_sync.rb must define Billing::InvoiceSync. Directories become namespaces. Acronyms need an inflection rule (inflect.acronym "API") if you want API rather than Api.

Modes:

  • Development: constants load lazily on first reference, and when files change Rails reloads them by removing the autoloaded constants and starting fresh on the next request. Never keep references to reloadable classes in places that survive reloads, such as initializers or class-level caches; use Rails.application.config.to_prepare for setup that must run after each reload.
  • Production and tests in CI: everything is eager loaded at boot, which is thread-safe, catches naming errors early and plays well with copy-on-write forking.

Run bin/rails zeitwerk:check to verify every file defines the expected constant.

Boot sequence: config/boot.rb sets up Bundler (and Bootsnap's load-path cache), config/application.rb requires the frameworks and defines your Application class, config/environments/*.rb applies per-environment settings, then initializers in config/initializers run in alphabetical order, and Railties (engines and gems) hook in with their own initializers. Finally, eager loading happens if enabled and the app is ready to serve.

Keep initializers small and side-effect free; heavy work there slows every boot, test run and deploy.

Example

# File -> constant mapping that Zeitwerk expects
# app/models/user_profile.rb             -> UserProfile
# app/models/billing/invoice.rb          -> Billing::Invoice
# app/services/payments/stripe_client.rb -> Payments::StripeClient
# lib/pdf_renderer.rb (autoload_lib)     -> PdfRenderer

# config/application.rb
module Blog
  class Application < Rails::Application
    config.load_defaults 8.0
    config.autoload_lib(ignore: %w[assets tasks])
  end
end

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) { |i| i.acronym "API" }
# now app/controllers/api/users_controller.rb -> API::UsersController

# config/initializers/payments.rb
Rails.application.config.to_prepare do
  Payments.gateway = Payments::StripeClient.new   # re-run after each code reload
end
# $ bin/rails zeitwerk:check
# Hold on, I am eager loading the application.
# All is good!

Remember

  • Zeitwerk maps file paths to constant names; directories are namespaces
  • Development autoloads and reloads; production eager loads at boot
  • Use to_prepare for setup that references reloadable classes; check with zeitwerk:check

Watch out for

  • Storing a reference to an app class in an initializer, which becomes stale after a reload in development.

Check yourself

  1. app/services/pdf/export.rb defines class PdfExport. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: guides.rubyonrails.org/autoloading_and_reloading_constants.html

Builds on

Unlocks

Nothing builds on this yet.