Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Ruby on Rails  Module 1 – Rails foundations

Conventions, autoloading and Zeitwerk

Apply the naming rules that tie a Rails model to its table, controller and files, see how Zeitwerk loads code, and fix autoloading errors.

  • Beginner
  • 10 minutes
  • Examples run with Ruby 4.0.7 and rails 8.1.4
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Apply Rails naming conventions to models, tables, foreign keys, controllers, views and files
  • Explain how Zeitwerk maps a file path to the constant it must define, and loads it on first use
  • Predict what eager loading in production changes for a misnamed file
  • Fix autoloading errors with bin/rails zeitwerk:check and acronym inflections
  • Move code that needs app classes at boot into a to_prepare block

Before you start

What you’ll learn · Before you start

What you will learn

  • Apply Rails naming conventions to models, tables, foreign keys, controllers, views and files
  • Explain how Zeitwerk maps a file path to the constant it must define, and loads it on first use
  • Predict what eager loading in production changes for a misnamed file
  • Fix autoloading errors with bin/rails zeitwerk:check and acronym inflections
  • Move code that needs app classes at boot into a to_prepare block

Before you start

On this page

Name a model Batch, and Rails already knows six more names: the table batches, the foreign key batch_id, the controller BatchesController, the folder app/views/batches, the file app/models/batch.rb and the route helper batches_path. Nobody writes these down, so one misspelt file name breaks a feature in a way that can stay hidden in development and stop production at boot.

This lesson takes those conventions apart on recorded runs: the names the inflector derives, how Zeitwerk turns file paths into constants and loads them, what changes when production loads everything at boot, and how to find and fix three naming mistakes that break autoloading.

One name ties a model to its table, controller, views and files

Rails derives every related name from the model’s name with Active Support’s inflector: The part of Active Support that turns one word into another by rules: plural and singular, CamelCase and snake_case, so that Batch gives batches and batch_id., the same code your app can call: tableize gives the table, foreign_key the column that points at it, pluralize the controller, underscore the file. The Rails Doctrine gives the reason in its own example: a belongs_to :account needs no setting, because the foreign key is account_id. This program asks the inflector for five models of Kaksha:

Every name Rails derives from a model's name Ruby · conventions.rb
# One model name and every name Rails derives from it: the table, the foreign key, the controller and the file.
# Active Support's inflector does the work, with the same rules Rails itself uses.
require "active_support/core_ext/string"

row = "%-11s %-12s %-14s %-22s %s"
puts format(row, "Model", "Table", "Foreign key", "Controller", "File")
%w[Batch FeePlan Enrollment Person Category].each do |model|
  puts format(row, model, model.tableize, model.foreign_key, "#{model.pluralize}Controller", "app/models/#{model.underscore}.rb")
end
puts
puts "Fees::ReceiptBuilder lives in app/services/#{'Fees::ReceiptBuilder'.underscore}.rb"
puts "The views of BatchesController are in app/views/#{'BatchesController'.delete_suffix('Controller').underscore}/"

Output

Model       Table        Foreign key    Controller             File
Batch       batches      batch_id       BatchesController      app/models/batch.rb
FeePlan     fee_plans    fee_plan_id    FeePlansController     app/models/fee_plan.rb
Enrollment  enrollments  enrollment_id  EnrollmentsController  app/models/enrollment.rb
Person      people       person_id      PeopleController       app/models/person.rb
Category    categories   category_id    CategoriesController   app/models/category.rb

Fees::ReceiptBuilder lives in app/services/fees/receipt_builder.rb
The views of BatchesController are in app/views/batches/

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby conventions.rb

Read the rows. Table names are plural and snake_case (fee_plans); foreign keys are singular with _id; controllers are plural (FeePlansController); files are singular snake_case. Person shows that the inflector knows irregular plurals (people), and Category shows the y to ies rule. A join model such as Enrollment gets its own ordinary names, so name join models for what they mean rather than as BatchStudent.

The last two lines show the rule for namespaces and views: Fees::ReceiptBuilder lives in app/services/fees/receipt_builder.rb, and BatchesController renders from app/views/batches/.

Ask the inflector before you name

When a name is unusual, run bin/rails runner 'puts "Syllabus".tableize' first. If the answer surprises you, pick another name or add an inflection before any table exists.

Checkpoint

Check: Names derived from a model's name

  1. Question 1 of 2 Which table does a model named FeePlan use by convention?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: fee_plans

    The inflector's tableize gives fee_plans, the foreign key fee_plan_id and the controller FeePlansController.

  2. Question 2 of 2 An enrollments table belongs to a person. Which column does Rails expect without any setting?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: person_id

    belongs_to :person looks for person_id, the way the Rails Doctrine's belongs_to :account looks for account_id.

Zeitwerk maps each file path to one constant

Rails code has no require lines for your own classes. Zeitwerk: The code loader Rails uses: it expects each file to define the constant its path names, loads the file on first use, and in development reloads the code after a file changes., the code loader Rails sets up, reads the autoload path: A root directory whose files Zeitwerk loads by name, such as app/models; a new app has every subdirectory of app/ except assets, javascript and views. instead: by default every subdirectory of app/ that exists at boot except assets, javascript and views, plus lib/ through the generated config.autoload_lib(ignore: %w[assets tasks]). Each file under one of those roots must define the one constant its relative path names. Step through three files:

Interactive · step by step From a file path to the constant it must define Step through the three files. Each step takes one path apart the way Zeitwerk does.
From a file path to the constant it must define, step 5 of 5. Showing models/fee_plan.rb, FeePlan, concerns/payable.rb, Payable, fees/receipt_builder.rb, Fees::ReceiptBuilder. In focus: Fees::ReceiptBuilder.models/fee_plan.rbroot: app/modelsFeePlanconcerns/payable.rbroot: app/models/concernsPayablefees/receipt_builder.rbroot: app/servicesFees::ReceiptBuilder

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Start from a root directory. Zeitwerk knows a list of root directories; in a Rails app they are the subdirectories of app/, among them app/models and app/services. A path is read from its root, never from app/.
  2. The file name, camelized. Below app/models, fee_plan.rb names one constant: fee_plan camelized is FeePlan. The file must define exactly that, and nothing has to require it.
  3. Concerns are a root of their own. app/models/concerns is itself a root directory, so payable.rb defines Payable at the top level, not Concerns::Payable.
  4. Folders become namespaces. Below app/services, the folder fees becomes the module Fees and the file receipt_builder.rb the class inside it: Fees::ReceiptBuilder.
  5. Loaded on first use. Setting up loads nothing. The first time code names Fees::ReceiptBuilder, Zeitwerk loads its file, and that file then loads Payable and FeePlan as it uses them, as the recorded run printed.

The program below uses Zeitwerk on its own, outside Rails, with the same three roots. It prints the constant each file must define, then uses Fees::ReceiptBuilder once and reports each file as Zeitwerk loads it:

Zeitwerk's map of three files, then loading on first use Ruby · zeitwerk_demo.rb
# Zeitwerk on its own, outside Rails, with three root directories laid out as a Rails app lays them out:
# app/models, app/models/concerns and app/services. It maps each file to the constant its path names, then loads a
# file only when its constant is first used.
require "zeitwerk"

loader = Zeitwerk::Loader.new
%w[app/models app/models/concerns app/services].each { |dir| loader.push_dir(File.join(__dir__, "lite", dir)) }
loader.on_load { |cpath, _value, abspath| puts "  loaded #{cpath.ljust(20)} from #{abspath.delete_prefix("#{__dir__}/")}" }
loader.setup

Dir.glob("lite/app/**/*.rb", base: __dir__).sort.each do |file|
  puts "#{file.ljust(42)} defines #{loader.cpath_expected_at(File.join(__dir__, file))}"
end
puts
puts "Nothing is loaded yet: FeePlan is #{Object.autoload?(:FeePlan) ? 'waiting to autoload' : 'loaded'}"
puts "First use of Fees::ReceiptBuilder:"
puts "  #{Fees::ReceiptBuilder.new.call}"
puts "payable? #{Fees::ReceiptBuilder.new.payable?}"

Output

lite/app/models/concerns/payable.rb        defines Payable
lite/app/models/fee_plan.rb                defines FeePlan
lite/app/services/fees/receipt_builder.rb  defines Fees::ReceiptBuilder

Nothing is loaded yet: FeePlan is waiting to autoload
First use of Fees::ReceiptBuilder:
  loaded Fees                 from lite/app/services/fees
  loaded Payable              from lite/app/models/concerns/payable.rb
  loaded Fees::ReceiptBuilder from lite/app/services/fees/receipt_builder.rb
  loaded FeePlan              from lite/app/models/fee_plan.rb
  Receipt for 1500000 paise
payable? true

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby zeitwerk_demo.rb

Before the first use, FeePlan was only set to autoload: Zeitwerk had told Ruby which file holds it, and read nothing. Using Fees::ReceiptBuilder loaded four things in order: the module Fees (made from the folder), Payable (included by the class body), the class itself, then FeePlan when call ran. Each file was read once, at the moment it was needed.

Key idea

Below its root directory, a file’s path names exactly one constant: folders become modules, the file name becomes the class, and Zeitwerk loads the file the first time that constant is used.

Checkpoint

Check: How Zeitwerk maps paths to constants

  1. Question 1 of 2 Which constant must app/services/fees/receipt_builder.rb define?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Fees::ReceiptBuilder

    Paths are read from their root directory: fees/receipt_builder.rb names Fees::ReceiptBuilder, as the recorded map printed.

  2. Question 2 of 2 Which constant must app/models/concerns/payable.rb define?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Payable

    Concerns folders are autoload paths themselves, which is why the recorded map shows payable.rb defining Payable.

Development loads on first use; production loads everything at boot

Development is set for quick edits: config.eager_load = false, so Zeitwerk loads files as they are used, and after you change a file Rails unloads the app’s classes before the next request, so they load again from the files. Production is set to load once: config.eager_load = true in the generated production.rb, so the app eager loading: Loading every file of the autoload paths when the app boots instead of on first use, as production does with config.eager_load = true.: it loads every file at boot, before it answers anything. The Autoloading guide gives two reasons: every class is ready before the first request, and eager loading is friendly to copy-on-write, so worker processes that a server forks after the boot share the loaded code instead of each loading its own copy. Tests eager-load only on CI: the generated test.rb sets config.eager_load = ENV["CI"].present?.

That difference decides when a naming mistake shows up. Three files in ../broken break the rule, and the session copies them into Kaksha and boots it both ways. Step through the two boots first:

Interactive · message by message The same mistake, in development and in production Step through both boots of the recorded session. The file defines Feeplan where its path promises FeePlan.
The same mistake, in development and in production, step 5 of 5. Messages so far: Rails boot to Zeitwerk: setup; Rails boot to Zeitwerk: eager_load; Zeitwerk to app/ files: load fee_plan.rb; Zeitwerk to Rails boot: Zeitwerk::NameError.Rails bootstopped, status 1Zeitwerkeager loadingapp/ filesdefines Feeplansetupeager_loadload fee_plan.rbZeitwerk::NameError

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Development boots. With config.eager_load = false, booting only asks Zeitwerk to set up: for each root, such as app/models, it tells Ruby which file or folder holds each top-level constant, and it reads no file.
  2. The script runs. The runner prints booted. Nothing used FeePlan, so the wrong file was never opened, and nothing went wrong.
  3. Production boots. Production has config.eager_load = true, so after setup Rails asks Zeitwerk to load every file at once, before the app answers anything.
  4. The wrong file is read. Zeitwerk loads app/models/fee_plan.rb and checks what it defined. It finds Feeplan, not FeePlan.
  5. The boot stops. Zeitwerk raises Zeitwerk::NameError naming the file and the constant it expected, and the production boot ends there with status 1.

The same mistakes, booted in development and in production Ruby · sessions/broken_session.rb
# Three files that break Zeitwerk's naming rule, copied into the Kaksha app (../broken holds them). Development boots
# anyway, because it loads a file only when its constant is used; production loads every file at boot.
# SECRET_KEY_BASE_DUMMY=1 stands in for the production secret, as the generated Dockerfile does, and Ruby's
# --backtrace-limit=1 keeps the error to its message and first line of the stack trace.
require_relative "session"

PRODUCTION = "RUBYOPT=--backtrace-limit=1 RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails runner 'puts \"booted\"'"

Session.run(<<~SH, fails: [PRODUCTION, "bin/rails zeitwerk:check"])
  cd ../kaksha
  cp -R ../broken/app ../broken/config .
  bin/rails runner 'puts "booted"'
  #{PRODUCTION}
  bin/rails zeitwerk:check
SH

Output

$ cd ../kaksha
$ cp -R ../broken/app ../broken/config .
$ bin/rails runner 'puts "booted"'
booted
$ RUBYOPT=--backtrace-limit=1 RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails runner 'puts "booted"'
…/lib/ruby/gems/4.0.0/gems/zeitwerk-2.8.3/lib/zeitwerk/loader/callbacks.rb:31:in 'Zeitwerk::Loader::Callbacks#on_file_autoloaded': expected file kaksha/app/models/fee_plan.rb to define constant FeePlan, but didn't (Zeitwerk::NameError)

      raise Zeitwerk::NameError.new(msg, cref.cname)
      ^^^^^
	from …/lib/ruby/gems/4.0.0/gems/zeitwerk-2.8.3/lib/zeitwerk/core_ext/kernel.rb:27:in 'Kernel#require'
	 ... 53 levels...
[exit status 1]
$ bin/rails zeitwerk:check
expected file app/models/fee_plan.rb to define constant FeePlan, but didn't
Hold on, I am eager loading the application.
[exit status 1]

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby broken_session.rb

Development printed booted: nothing used FeePlan, so the wrong file was never opened. Production stopped at boot with Zeitwerk::NameError: expected file kaksha/app/models/fee_plan.rb to define constant FeePlan, but didn't. A mistake that development hides until someone opens the right page, production refuses outright.

Trusting development to catch naming errors

A misnamed file that no page in development touched still boots there. Run bin/rails zeitwerk:check, which eager loads like production, before every deploy and in CI.

Checkpoint

Check: Loading on use and eager loading

  1. Question 1 of 2 In the broken session, why did development print "booted" while production stopped at boot?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Development loads a file only when its constant is used, and production loads every file at boot

    config.eager_load is false in development and true in production, so a misnamed file fails at the first use in development and at boot in production.

  2. Question 2 of 2 What does production gain by loading every file at boot, by the Autoloading guide's account?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Every class is ready before the first request, and worker processes forked after the boot share the loaded code

    Eager loading makes the boot slower so that no request waits on a file, and worker processes forked after the boot share the code it loaded.

zeitwerk:check finds files that define the wrong constant

bin/rails zeitwerk:check loads the whole app the way production would and reports the first file that does not define its constant; the session above shows it stopping at fee_plan.rb. Its error sits above its first line, “Hold on, I am eager loading the application.”, because the error goes to standard error, which Ruby writes at once, while that line waited in the output buffer until the task ended. To see every broken file at once, this program loads each file of ../broken on its own with Zeitwerk:

All three naming errors, one file at a time Ruby · broken_names.rb
# The three files of ../broken, each loaded on its own by Zeitwerk (zeitwerk:check stops at the first problem, this
# reports every one). Each error names the constant the file's path promises.
require "zeitwerk"

loader = Zeitwerk::Loader.new
%w[app/models app/services].each { |dir| loader.push_dir(File.join(__dir__, "broken", dir)) }
loader.setup

Dir.glob("broken/app/**/*.rb", base: __dir__).sort.each do |file|
  loader.load_file(File.join(__dir__, file))
  puts "#{file}: ok"
rescue Zeitwerk::NameError => e
  puts "#{file}:"
  puts "  #{e.message.sub("#{__dir__}/", '')}"
end

Output

broken/app/models/fee_plan.rb:
  expected file broken/app/models/fee_plan.rb to define constant FeePlan, but didn't
broken/app/models/pdf_receipt.rb:
  expected file broken/app/models/pdf_receipt.rb to define constant PdfReceipt, but didn't
broken/app/services/fees/receipt_builder.rb:
  expected file broken/app/services/fees/receipt_builder.rb to define constant Fees::ReceiptBuilder, but didn't

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby broken_names.rb

Each error names the constant the path promises. fee_plan.rb defined Feeplan instead of FeePlan: a spelling fix. receipt_builder.rb sits in the folder fees, so it must define Fees::ReceiptBuilder: a missing namespace. pdf_receipt.rb defined PDFReceipt while the path promises PdfReceipt: an acronym, which the next section fixes without renaming anything. Here are the three fixes and the check that follows them:

The fixes, then zeitwerk:check and a production boot Ruby · sessions/fixed_session.rb
# The same three files fixed (../fixed), what changed, and Zeitwerk's check and a production boot afterwards.
require_relative "session"

Session.run(<<~SH, fails: ["diff -r ../broken ../fixed"])
  diff -r ../broken ../fixed
  cd ../kaksha
  cp -R ../fixed/app ../fixed/config .
  bin/rails zeitwerk:check
  RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails runner 'puts Fees::ReceiptBuilder.new.call'
SH

Output

$ diff -r ../broken ../fixed
diff -r ../broken/app/models/fee_plan.rb ../fixed/app/models/fee_plan.rb
1c1
< class Feeplan
---
> class FeePlan
diff -r ../broken/app/services/fees/receipt_builder.rb ../fixed/app/services/fees/receipt_builder.rb
1,2c1,4
< class ReceiptBuilder
<   def call = "Receipt for #{FeePlan.default_paise} paise"
---
> module Fees
>   class ReceiptBuilder
>     def call = "Receipt for #{FeePlan.default_paise} paise"
>   end
diff -r ../broken/config/initializers/inflections.rb ../fixed/config/initializers/inflections.rb
16a17,20
> 
> ActiveSupport::Inflector.inflections(:en) do |inflect|
>   inflect.acronym "PDF"
> end
[exit status 1]
$ cd ../kaksha
$ cp -R ../fixed/app ../fixed/config .
$ bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!
$ RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails runner 'puts Fees::ReceiptBuilder.new.call'
Using vips to process variants requires the libvips library. Please install libvips using the instructions on the libvips website.
Receipt for 1500000 paise

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby fixed_session.rb

After the fixes the check printed “All is good!” and production booted far enough to run the receipt builder. The libvips line has nothing to do with autoloading. Active Storage needs the libvips image library for image variants, and Rails does not install it, so at the end of a boot Active Storage logs that warning when it cannot load the library, as on this machine; the generated production.rb sends the log to standard output, so the production boot shows it. Put the check in CI beside the tests: it ends with status 1 on a broken name, as the first session showed, so the build stops before the deploy does.

Checkpoint

Check: Finding naming errors with zeitwerk:check

  1. Question 1 of 2 broken/app/services/fees/receipt_builder.rb defined class ReceiptBuilder. Which fix makes Zeitwerk accept it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Define it inside module Fees, as the fixed file does

    Match the constant to the path: module Fees around the class, or move the file to app/services/receipt_builder.rb.

  2. Question 2 of 2 The broken app had three misnamed files. How many did bin/rails zeitwerk:check report?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: One, the first it reached, fee_plan.rb

    zeitwerk:check raises on the first problem, so fix and re-run until it prints "All is good!".

Acronyms need an inflection

Zeitwerk in Rails uses the inflector to camelize file names, and the inflector does not know your acronyms: pdf_receipt becomes PdfReceipt. You can write the class as PdfReceipt, or tell the inflector that PDF is an acronym, in config/initializers/inflections.rb, as the Autoloading guide does for HTML and SSL:

camelize and underscore, before and after an acronym Ruby · inflection_demo.rb
# How Rails turns a file name into a class name (camelize) and back (underscore), before and after an acronym
# inflection like the one config/initializers/inflections.rb can hold.
require "active_support/core_ext/string"

puts "Before: pdf_receipt -> #{'pdf_receipt'.camelize}, PDFReceipt -> #{'PDFReceipt'.underscore}"
ActiveSupport::Inflector.inflections(:en) { |inflect| inflect.acronym "PDF" }
puts "After:  pdf_receipt -> #{'pdf_receipt'.camelize}, PDFReceipt -> #{'PDFReceipt'.underscore}"
puts "        pdf_receipts_controller -> #{'pdf_receipts_controller'.camelize}"

Output

Before: pdf_receipt -> PdfReceipt, PDFReceipt -> pdf_receipt
After:  pdf_receipt -> PDFReceipt, PDFReceipt -> pdf_receipt
        pdf_receipts_controller -> PDFReceiptsController

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby inflection_demo.rb

After inflect.acronym "PDF", pdf_receipt camelizes to PDFReceipt, and so does every name camelized from the word: the controller that resources :pdf_receipts looks for becomes PDFReceiptsController. underscore still gives pdf_receipt, so file names, the table pdf_receipts and route helpers such as pdf_receipts_path stay as they were. This is the third fix of the session: one line in the initializer, no file renamed. The generated initializer shows the same form in a comment, with RESTful as its example, and switches nothing on, so each acronym your names use is yours to add.

Adding an acronym after code uses the other spelling

An acronym changes every name camelized from the word, in the whole app. Once PDF is an acronym, a file pdf_invoice.rb that still defines PdfInvoice breaks, because its path now promises PDFInvoice. Run bin/rails zeitwerk:check right after you add an acronym, and fix what it reports until it prints “All is good!”.

Checkpoint

Check: Acronym inflections

  1. One question app/models/pdf_receipt.rb defines class PDFReceipt. What makes Zeitwerk accept it without renaming anything?

    Choose one answer.

    Show the answer: One question

    Answer: inflect.acronym "PDF" in config/initializers/inflections.rb

    An acronym inflection makes camelize keep PDF in capitals, so the file and the class agree.

Code that runs at boot must wait for the app’s classes

Files in config/initializers/ run while the app boots, before Zeitwerk manages app/. The Autoloading guide is explicit: initializers cannot refer to reloadable constants. Kaksha prints its receipts on A4 paper, so an initializer sets that on PDFReceipt, a class of app/models. The session tries it, then moves the line into to_prepare:

Configuring an app class at boot, then in to_prepare Ruby · sessions/initializer_session.rb
# An initializer that configures an app class while the app boots, then the same line inside to_prepare
# (../initializers holds both versions; ../fixed holds the corrected app files). Ruby's --backtrace-limit=1 keeps the
# error to its message and first line of the stack trace. The last command prints the paper size, sets another one by
# hand, reloads the app's code the way reload! does in a console, and prints the size again.
require_relative "session"

EARLY = "RUBYOPT=--backtrace-limit=1 bin/rails runner 'puts PDFReceipt.paper_size'"
RELOAD = %q(bin/rails runner 'puts PDFReceipt.paper_size; PDFReceipt.paper_size = "A5"; Rails.application.reloader.reload!; puts PDFReceipt.paper_size')

Session.run(<<~SH, fails: [EARLY])
  cd ../kaksha
  cp -R ../fixed/app ../fixed/config .
  cp ../initializers/early.rb config/initializers/receipts.rb
  #{EARLY}
  cp ../initializers/prepared.rb config/initializers/receipts.rb
  #{RELOAD}
SH

Output

$ cd ../kaksha
$ cp -R ../fixed/app ../fixed/config .
$ cp ../initializers/early.rb config/initializers/receipts.rb
$ RUBYOPT=--backtrace-limit=1 bin/rails runner 'puts PDFReceipt.paper_size'
kaksha/config/initializers/receipts.rb:2:in '<main>': uninitialized constant PDFReceipt (NameError)

PDFReceipt.paper_size = "A4"
^^^^^^^^^^
	from …/lib/ruby/gems/4.0.0/gems/railties-8.1.4/lib/rails/engine.rb:692:in 'Kernel#load'
	 ... 50 levels...
[exit status 1]
$ cp ../initializers/prepared.rb config/initializers/receipts.rb
$ bin/rails runner 'puts PDFReceipt.paper_size; PDFReceipt.paper_size = "A5"; Rails.application.reloader.reload!; puts PDFReceipt.paper_size'
A4
A4

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby initializer_session.rb

config/initializers/receipts.rb named PDFReceipt and the boot failed with uninitialized constant PDFReceipt (NameError). Wrapped in Rails.application.config.to_prepare do … end, the same line runs later in the boot, once Zeitwerk manages app/, so the next run printed A4. That run then set A5 by hand and reloaded the code the way reload! does in a console, and the size was A4 again. A reload replaces the app’s classes with new ones loaded from their files, and an initializer never runs again after boot, so nothing it set on the old PDFReceipt would reach the new one. to_prepare runs at boot and again after every reload, so each new class gets its paper size. The guide warns that the block may run twice, so it should give the same result each time, as setting a value does.

Boot code comes in three kinds, and each has its own place:

Code that must run at boot Where it belongs
Settings that need no app class (config.x.*, a gem’s options) An initializer, as it is
Code that configures a reloadable class from app/ Rails.application.config.to_prepare
Code that extends a Rails framework class An ActiveSupport.on_load hook, such as on_load(:action_controller)

Checkpoint

Check: App classes in code that runs at boot

  1. Question 1 of 2 The receipt builder gets a class-level footer, and config/initializers/receipts.rb sets Fees::ReceiptBuilder.footer = "Kaksha Academy". The boot stops with uninitialized constant Fees. What is the fix?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Wrap the line in Rails.application.config.to_prepare do … end

    Fees is the module Zeitwerk makes from the folder app/services/fees, and it does not exist yet while initializers run. Inside to_prepare the line runs after boot, as the session's receipts.rb did.

  2. Question 2 of 2 The session's last run printed A4, set the paper size to A5 by hand, reloaded the code and printed the size again. Why was it A4?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: to_prepare ran again after the reload and set A4 on the reloaded PDFReceipt

    A reload replaces the app's classes with new ones loaded from their files, and to_prepare configures them again; a value set anywhere else stays with the old class.

Practise: name it, load it, fix it

Most questions use this lesson’s runs: the inflector’s names, Zeitwerk’s map, the two boots, the three broken files and the receipt initializer; a few ask you to carry an idea to a new case. Each choice explains itself:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What does Rails name the controller for the Person model?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: PeopleController

    The recorded table shows Person, people, person_id, PeopleController.

  2. Question 2 of 12 Where does FeePlansController#show look for its template by default?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: app/views/fee_plans/show.html.erb

    BatchesController renders from app/views/batches/, so FeePlansController renders from app/views/fee_plans/.

  3. Question 3 of 12 In the Zeitwerk demo, when was app/models/fee_plan.rb read?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: When Fees::ReceiptBuilder#call first used FeePlan

    Zeitwerk sets an autoload for each constant at setup and reads the file on first use.

  4. Question 4 of 12 You add the folder app/forms with sign_up_form.rb defining SignUpForm. What else must you configure?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Nothing, once the app restarts; every subdirectory of app/ that exists at boot is an autoload path

    New folders under app/ become autoload paths at the next boot, and their files define top-level constants.

  5. Question 5 of 12 Which setting makes production load all of app/ at boot?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: config.eager_load = true

    eager_load is true in production, false in development and set from CI in test.

  6. Question 6 of 12 app/models/payment.rb defines Payment, and three files beside it define its subclasses CardPayment, CashPayment and ChequePayment. A page lists Payment.subclasses. Right after a restart, development lists none of them, while production lists all three. Why?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Development reads a subclass's file only when its name is used, while production loads every file at boot

    Lazy loading defines a class when its name is first used, so code that asks for every subclass sees only the loaded ones. The Autoloading guide's section on single table inheritance deals with exactly this.

  7. Question 7 of 12 What does bin/rails zeitwerk:check do?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Loads the whole app the way production does and reports a file that does not define its constant

    zeitwerk:check is a quick production-like eager load, made for CI and before deploys.

  8. Question 8 of 12 The check says: expected file app/models/fee_plan.rb to define constant FeePlan. The file defines Feeplan. What do you change?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: The class name to FeePlan

    Make the constant and the path agree; the usual fix is the class name.

  9. Question 9 of 12 After inflect.acronym "PDF", what does pdf_receipts_controller camelize to?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: PDFReceiptsController

    An acronym changes every camelized name that contains the word: classes, controllers and constants.

  10. Question 10 of 12 After inflect.acronym "PDF", which file must define PDFReceipt?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: pdf_receipt.rb, unchanged

    The acronym changes camelize, not the file names, which is why it fixes the class without a rename.

  11. Question 11 of 12 Why did the session's first boot fail with uninitialized constant PDFReceipt?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: Initializers run while the app boots, before the app's reloadable classes can autoload

    Boot code that configures an app class waits in to_prepare, which runs after boot and after every reload.

  12. Question 12 of 12 Boot code must add a helper method to every controller. Where does it belong?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: In an ActiveSupport.on_load(:action_controller) hook

    Settings go in initializers, app classes in to_prepare, and framework extensions in on_load hooks.

Interview questions

Warm-up: why does a Rails app need no require lines for its own models and controllers?

Because Zeitwerk loads them. At boot Rails gives Zeitwerk the autoload paths, every subdirectory of app/ such as app/models and app/controllers, and Zeitwerk expects each file to define the constant its path names below its root: app/models/fee_plan.rb defines FeePlan, app/services/fees/receipt_builder.rb defines Fees::ReceiptBuilder. In development it loads a file the first time its constant is used and reloads the code after a file changes; in production it loads every file at boot. A file that defines a different constant raises Zeitwerk::NameError, which bin/rails zeitwerk:check reports before a deploy.

Two questions of this module’s bank build on this lesson: a deploy that fails with a Zeitwerk::NameError that nobody saw in development, and an initializer that reads a value from a class of app/.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A deploy fails at boot with Zeitwerk::NameError for app/models/sms_sender.rb, yet every page worked in development and the tests passed on laptops. Explain why, fix it, and stop it from happening again.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    An initializer stores FeePlan.default_paise in a setting so that every request can read it. It fails at boot in Rails 8.1. Why does Rails refuse it, what is the right place for that code, and what would go wrong if Rails allowed it?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. From the model name Batch, which other names does Rails work out?

    The table batches, the foreign key batch_id, the controller BatchesController and the views folder batches, all through the inflector and with no configuration.

  2. Which constant must the file app/services/fees/receipt_builder.rb define?

    Fees::ReceiptBuilder. Below the autoload root, each folder becomes a module and the file name becomes the class.

  3. Why can a misnamed file work in development and still stop a production boot?

    Development loads a constant only when code first uses it, so an unused file is never read. Production eager-loads every file at boot, where the wrong constant raises Zeitwerk::NameError.

  4. What does bin/rails zeitwerk:check report?

    It loads the app the way production does and names the first file that defines a different constant from the one its path names, before a deploy finds it.

  5. A class is named PDFReceipt. What does Zeitwerk need before it can load it?

    An inflect.acronym entry for PDF, or the class renamed to the spelling the inflector already gives, PdfReceipt.

  6. Where does boot code go that configures your app's own classes?

    Inside a to_prepare block. Initializers run once, before those classes can autoload; to_prepare runs at boot and again after each reload.

Key takeaways

  • One model name gives every related name through the inflector: table, foreign key, controller, views folder and file, so Batch means batches, batch_id and BatchesController without configuration.
  • Zeitwerk expects each file under an autoload path to define the constant its relative path names, folders as modules, and loads it the first time the constant is used.
  • Development loads on first use and reloads changes, while production eager-loads everything at boot, so a misnamed file can boot in development and stop production.
  • bin/rails zeitwerk:check loads the app like production and names the first broken file; a class name with an acronym needs inflect.acronym, or the spelling the inflector already gives, such as PdfReceipt.
  • Initializers run once, before the app’s classes can autoload; code that configures those classes goes in a to_prepare block, which runs at boot and again after every reload.

Terms in this lesson

autoload path
A root directory whose files Zeitwerk loads by name, such as app/models; a new app has every subdirectory of app/ except assets, javascript and views.
eager loading
Loading every file of the autoload paths when the app boots instead of on first use, as production does with config.eager_load = true.
inflector
The part of Active Support that turns one word into another by rules: plural and singular, CamelCase and snake_case, so that Batch gives batches and batch_id.
Zeitwerk
The code loader Rails uses: it expects each file to define the constant its path names, loads the file on first use, and in development reloads the code after a file changes.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.