Treat Rails for What it Is and Organize Code By Structural Purpose
October 25, 2022 📬 Get My Weekly Newsletter ☞
If I had one piece of advice for using Rails, it is to treat Rails for what it is, not what you might like it to be. This was the subject of my talk at Rails Conf 2022, but it has a few practical implications for, among other things, how to organize the code in your Rails app.
No matter how simple your app is, it will have business logic, and you will have a boundary
between your controllers/jobs/mailboxes/tasks and that logic. The code that defines that
boundary is called a service layer. It should go in app/services
, because Rails organizes
code by structural purpose, not domain purpose. Following Rails conventions will make
development of your app easier to sustain over time.
An App’s Architecture Starts with Code Organization
The reason the question “where does this code go?” is important to answer is because it’s the foundation of the application’s architecture. If developers don’t know (or can’t agree on) where code is supposed to go, sustained development is going to be difficult.
Rails is a framework that will manage most of the classes you need to build an app: HTTP, Email, database, background jobs, etc. Rails does not provide any specific way to manage your core domain or business logic. The only feature it provides is the ability to auto-load classes in app/«whatever»
.
The easier it is to answer the question “where does the code go?” the easier it will be to work on your app over time and through change (in requirements, team, etc). If answering this question is difficult, change is harder. Questions that are difficult to answer create more friction than questions that are easy to answer, and you want to reduce friction, especially around questions that must be answered before coding can start.
Fortunately, Rails does provide an easier answer for almost all of the code you have to
write: controllers go in app/controllers
, Active Records go in app/models
, Mailers go in app/mailers
, and so on. Developers don’t need to do a lot of analysis to figure out where that sort of code goes. So it should be with business logic.
Business (or perhaps domain) logic doesn’t fit into any of the Rails-managed classes, and Rails doesn’t provide an answer for where this code goes. I find it useful to acknowledge the boundary between Rails-managed classes and business logic, and I find the best term for this boundary to be service layer.
Business Logic Should be Encapsulated
Even if you don’t explicitly define a boundary between your controllers and business logic, it doesn’t mean it doesn’t exist, at least conceptually. This boundary is called a service layer, which Martin Fowler defines thusly, emphasis mine:
A Service Layer defines an application’s boundary and its set of available operations from the perspective of interfacing client layers. It encapsulates the application’s business logic, controlling transactions and coordinating responses in the implementation of its operations.
The highlighted section is important. The service layer encapsulates the business logic from “clients”. In a Rails app, a client is a controller, mailbox, task, or background job. They invoke business logic and interpret its results. The service layer is where this happens.
This does not imply that the service layer contain all the logic. It is just a boundary. It encapsulate whatever the logic is.
Rails developers often fail to create an explicit service layer, and have methods littered all over the place—often on Active Records—that trigger (and implement) business logic. This is needlessly confusing and hard to manage over time. Having instead a single place where business logic is invoked makes everything easier.
Inside app/services
, because of encapsulation, you are free to organize the code however you
like. If you prefer stateless procedures, you can do that. If you prefer a rich collection of
objects passing messages, you can do that, too. If you need to create subdirectories for domain
concepts, you can do that as well. This is the primary benefit of encapsulation and, because
this code is tucked into app/services
, it also is consistent with Rails’ conventions.
What About Decorators and Other Classes?
Most apps should not need more than app/services
plus what Rails gives you, but if you do end
up having a lot of classes that conform to some structural purpose, you can certainly create a
directory in app
to store them.
This is what Rails intends you to do, and creating a directory like app/decorators
is a clear way to communicate that there is a concept of a decorator, and that the way it is constructed is important to be consistent.
This provides an easy answer to “where do decorators go?” and is also consistent with how Rails wants you to structure your code. The more easy answers your architecture provides, the better.
What About lib
?
There is a second type of code that is particular to your app but doesn’t fall into a
Rails-managed class or business logic, and that is the code that should go in lib
. This
code is often infrastructure-type code like middleware or plugins. lib
can also hold code you
intend to extract as a gem in the future.
This convention follows the policy we’ve been discussing: easy answer to where code goes. If you
need to create some code that is not business logic and does not go in a Rails-managed class, it
goes in lib
.
What About Organizing by Domain Concept?
There are advantages to organizing code by domain concept instead of structural purpose. For
example, you might want app/shopping
to contain all the code about purchasing from your store
and app/reporting
to contain all the reporting.
To entirely organize your app this way requires quite the configuration feat with Rails and would obviate may of Rails’ benefits. It also creates a far more difficult-to-answer to the question of where code goes. Is there an existing concept where this new code should go? I there an existing concept that is close, and if we rename it would this code go there? Or, does adding this code to an existing concept make that concept too complex such that it requires splitting up into two smaller concepts?
These questions can be hard to answer, especially if the app is undergoing rapid change. You may
not know what concepts the app will need or if a concept will be developed beyond the initial
feature. When organizing by structural purpose you can always safely put the code in
app/controllers
or app/services
or wherever, and organize it later.
The ability to organize later is powerful: it’s much easier to organize code that exists and is tested than it is to try to predict where code should go in the future. The contents of your structurally-based folders will show you exactly what concepts are important and which ones aren’t.
When app
contains structurally-organized directories and those directories contain
domain-organized code, you get the best of both worlds. You can group by domain concept, but
also easily answer the question of where code goes. You also have an escape hatch if the
question is too hard to answer: put it in the top of the relevant directory.
Treat Rails for What it Is
Rails is a web framework that organizes code by structural purpose. It provides rudimentary tools
for adding your own new structural purposes, and does not prescribe how the code inside should be
organized. Thus, define your service layer explicitly in app/services
, then organize the code
in there—as well as your domain logic—however you see fit. If you always try to treat Rails for
what it is—not what you might like it to be—your app’s architecture will provide a solid
foundation for sustainable development.