February 25, 2019·4 min read·engineeringrubyrailszeitwerk

Rails 6 beta 2 replaces constant lookup guesses with Zeitwerk

Rails 6 beta 2 integrates Zeitwerk for autoloading. The change aligns constant lookup with Ruby semantics and makes file naming part of an explicit loading contract.

A class can have the correct name while its behavior still depends on which constant was loaded first. That is a difficult dependency to see in source review because the file itself does not describe the relevant execution history. Rails 6.0.0 beta 2, released February 25, changes the autoloading implementation to Zeitwerk. The release identifies this as its main structural change.

Xavier Noria's February 22 integration explanation describes both the mechanism and the beta-specific configuration. Zeitwerk establishes autoloads using absolute file names after a directory pass, descending into subdirectories when their namespaces are used. A missing constant no longer requires the same search through autoload paths for a relative file match.

I find Ruby easier to read when its object model and naming conventions explain what the program does. Loading behavior that depends on accidental execution order weakens that advantage. Zeitwerk's value is that it makes the connection between a constant and its defining file more deliberate.

File organization becomes a contract

Rails already encourages a correspondence between file names and the main constants they define. The new loader relies on that relationship to establish loading instructions. A file in an autoloaded tree is therefore part of the program's structure, rather than merely a container that can define arbitrary unrelated top-level names.

For example, a file intended to define Invoice should make that its main constant. An auxiliary type can live inside Invoice if that relationship is appropriate. Defining another unrelated top-level constant in the same file obscures which loading instruction is responsible for it. Noria specifically warns that this can produce incorrect reloading behavior.

The distinction matters because initial loading and reloading are different operations. A program can appear correct after its first boot because several constants happen to exist. Removing and reloading one named component can expose a second constant that the loader did not know it owned. An apparently convenient file organization can therefore create behavior that only appears after a source edit.

Treating the file-to-constant relationship as a contract makes that problem inspectable. A reviewer can compare a file path with its principal definition. The relevant information exists in the source tree, without requiring knowledge of which request or initializer happened to execute first.

Ruby constant lookup retains its lexical meaning

Noria calls out support for constant paths in class definitions. This declaration can now autoload with Ruby's expected semantics.

class Admin::UsersController < ApplicationController
end

The point is not that every spelling of a namespace becomes equivalent. Ruby's lexical rules still matter. The loader should supply missing definitions without inventing a different lookup model to compensate for how dependencies were discovered.

That separation helps keep language knowledge reusable. A developer reading a class definition should reason about Ruby's constant lookup rules, then separately about whether the relevant constant is available. If the loader changes the effective lookup behavior based on earlier execution, those concerns become entangled.

The new integration also claims general thread-safe autoloading, including multithreaded scripts run through bin/rails runner. That extends beyond the explicitly locked request paths supported before. It concerns safe loading of definitions, however, rather than making every mutable object created by those definitions safe for concurrent use.

A model class loaded safely can still contain shared state with no synchronization. Likewise, a script can load every constant correctly while racing over an application cache. The autoloader removes one category of coordination work. Application-level concurrency remains a separate responsibility.

Beta configuration preserves a migration choice

Rails 6 beta 2 has :zeitwerk and :classic modes through config.autoloader. Loading the Rails 6 defaults enables Zeitwerk by default for CRuby. The integration post shows an explicit way to opt back into classic mode after those defaults.

config.load_defaults "6.0"
config.autoloader = :classic

This is a migration control for the beta's behavior. It permits a project to separate adopting the framework version from adopting the new loader while examining compatibility. It should not be mistaken for the final word on every later version's API, because Noria describes the integration as exploratory.

The beta also freezes autoload paths after boot and ignores configured root directories absent at boot. Existing roots can still gain new subdirectories. Those rules give the loader a defined initial set of trees to manage. Creating an entirely new root later is a different operation from creating another namespace directory under an established root.

There is another distinction for namespace definitions. A file defining a class or module used as a namespace must use the class or module keyword. Assigning Class.new to that constant is not interchangeable for this purpose. Dynamic Ruby can express both forms, but the loader needs the declaration pattern appropriate to managing nested definitions.

Loading and reloading have different owners

The beta exposes main and once autoloaders. Its main instance manages application code that participates in reloading. Its once instance handles code that can autoload and eager load but does not participate in the same reload cycle, including engines loaded as gems in this integration.

That division expresses a real lifecycle difference between editable application source and installed framework dependencies. Application source changes during development, while installed gem code ordinarily remains fixed for the process. Reloading both categories indiscriminately would give them the same lifecycle despite their different origins. Explicit ownership lets the framework apply the intended behavior to each tree.

I regard that ownership as more useful than simply having fewer explicit require_dependency calls. Removing those calls is valuable when the loader has enough structural information to do the work correctly. The deeper improvement is knowing which files define a namespace and which loader controls their lifetime.

Noria also points out that Active Record single-table inheritance needs its hierarchy loaded to generate correct SQL. Autoloading a constant only when first referenced does not automatically satisfy a query that needs knowledge of all relevant subclasses. The beta supplies a preloading approach for that case. Lazy discovery remains appropriate for ordinary constant access, while a framework operation that needs a complete hierarchy has a stronger requirement.