Update: Rails v7.1 will introduce a new configuration method
config.autoload_lib to make it safer and easier to autoload the
/lib directory and explicitly exclude directories from autoloading. When released, this advice may no longer be relevant, though I imagine it will still be possible for developers to twist themselves into knots and cause outages with autoloading overrides.
One of the most common problems I encounter consulting on Rails projects is that developers have previously added
lib/ to autoload paths and then twisted themselves into knots creating error-prone, project-specific conventions for subsequently un-autoloading a subset of files also in
Don’t do it. Don’t add your Rails project’s
lib/ to autoload paths.
How does this happen?
A growing Rails application will accumulate a lot of ruby classes and files that don’t cleanly fit into the default
app/ directories of
models. Developers should also be creating new directories in
app/* to organize like-with-like files (your
app/merchants/, etc.). That’s ok!
But frequently there are one-off classes that don’t seem to rise to the level of their own directory in
app/. From looking through the cruft of projects like Mastodon or applications I’ve worked on, these files look like:
- A lone form builder
- POROs (“Plain old Ruby objects”) like
Pagination. Objects that serve a single purpose and are largely singletons/identity objects within the application.
- Boilerplate classes for 3rd party gems, e.g.
That these files accumulate in a project is a fact of life. When choosing where to put them, that’s when things can go wrong.
In a newly built Rails project
lib/ looks like the natural place for these. But
lib/ has a downside:
lib/ is not autoloaded. This can come as a surprise, even to experienced developers, because they have been accustomed to the convenience of autoloaded files in
app/. It’s not difficult to add an explicit
require statement into
application.rb or in an initializer, but that may not be one’s first thought.
That’s when people jump to googling “how to autoload
lib/”. Don’t do it!
lib/ should not be autoloaded.
The problem with autoloading
lib/ is that there will subsequently be files added to
lib/ that should not be autoloaded; because they should only be provisionally loaded in a certain environment or context, or deferred, for behavioral, performance, or memory reasons. If your project has already enabled autoloading on
lib/, it’s now likely you’ll then add additional configuration to un-autoload the new files. These overrides and counter-overrides accumulate over time and become difficult to understand and unwind, and they cause breakage because someone’s intuition of what will or won’t be loaded in a certain environment or context is wrong.
What should you do instead?
An omakase solution
lib/is intended to be for non-app specific library code that just happens to live in the app for now (usually pending extraction into open source or whatever). Everything app specific that’s part of the domain model should live in
app/models(that directory is for POROs as much as ARs)… Stuff like a generic PhoneNumberFormatter is exactly what
lib/is intended for. And if it’s app specific, for some reason, then
The omakase solution is to manually require files from
lib/ or use
app/models generically to mean “Domain Models” rather than solely Active Record models. That’s great! Do that.
A best practice
Xavier Noria, Zeitwerk’s creator writes:
The best practice to accomplish that nowadays is to move that code to
app/lib. Only the Ruby code you want to reload, tasks or other auxiliary files are OK in
Sidekiq’s Problems and Troubleshooting explains:
lib/directory will only cause pain. Move the code to
app/lib/and make sure the code inside follows the class/filename conventions.
This “best practice” is not without contention, as usually anything in Rails that deviates from omakase does, like RSpec instead of MiniTest or FactoryBot instead of Fixtures. But creating
app/lib as a convention for Rails apps works for me and many others.
Really, don’t autoload
Whatever path you take, don’t take the path of autoloading