Influenced by the experiences I’ve had last over many years of building and maintaining Rails applications, combined with my experiences using other technologies, I’ve developed some ways of structuring Rails applications that have worked out pretty well for me.
Some of my organizational tactics follow conventional wisdom, like keeping controllers thin. Other of my tactics are ones I haven’t really seen in others’ applications but wish I would.
Here’s an overview of the topics I touch on in this post.
- The lib folder
- Background jobs
- Service objects
- How I think about Rails code organization in general
Let’s start with controllers.
The most common type of controller in most Rails applications is a controller that’s based on an Active Record resource. For example, if there’s a
customers database table then there will also be a
Customer model class and a controller called
Controllers can start to get nasty when there get to be too many “custom” actions beyond the seven RESTful actions of
Let’s say we have a
CustomersController that, among other things, allows the user to send messages about a customer (by creating instances of a
Message, let’s say). The relevant actions might be called
create_message. This is maybe not that bad, but it clutters up the controller a little, and if you have enough custom actions on a controller then the controller can get pretty messy and hard to comprehend.
What I like to do in these scenarios is create a “custom” controller called e.g.
CustomerMessagesController. There’s no database table called
customer_messages or class called
CustomerMessage. The concept of a “customer message” is just something I made up. But now that this idea exists, my
CustomersController#create_message actions can become
CustomerMessagesController#create. I find this much tidier.
And as long as I’m at it, I’ll even create a PORO (plain old Ruby object) called
CustomerMessage where I can handle the business of creating a new customer message as not to clutter up either
Message with this stuff which is really not all that relevant to either of those classes. I might put a
create! method on
CustomerMessage which creates the appropriate
Message for me.
Furthermore, I’ll also often put
include ActiveModel::Model into my PORO so that I can bind the PORO to a form as though it were a regular old Active Record model.
Pieces of code are easier to understand when they don’t require you to also understand other pieces of code as a prerequisite. To use an extreme example to illustrate the point, it would obviously be impossible to understand a program so tangled with dependencies that understanding any of it required understanding all of it.
So, anything we can do to allow small chunks of our programs understandable in isolation is typically going to make our program easier to work with.
Namespaces serve as a signal that certain parts of the application are more related to each other than they are to anything else. For example, in the application I work on at work, I have a namespace called
Billing. I have another namespace called
Schedule. A developer who’s new to the codebase could look at the
Schedule namespaces and rightly assume that when they’re thinking about one, they can mostly ignore the other.
Some of my models are sufficiently fundamental that it doesn’t make sense to put them into any particular namespace. I have a model called
Appointment that’s like this. An
Appointment is obviously a scheduling concern a lot of the time, but just as often it’s a clinical concern or a billing concern. An appointment can’t justifiably be “owned” by any one namespace.
This doesn’t mean I can’t still benefit from namespaces though. I have a controller called
Billing::AppointmentsController which views appointments through a billing lens. I have another controller called
Chart::AppointmentsController which views appointments through a clinical lens. For scheduling, we have two calendar views, one that shows one day at a time and one that shows one month at a time. So I have two controllers for that:
Schedule::ByMonthCalendar::AppointmentsController. Imagine trying to cram all this stuff into a single
AppointmentsController. This idea of having namespaced contexts for broad models has been very useful.
I of course keep my models in
app/models just like everybody else. What’s maybe a little less common is the way I conceive of models. I don’t just think of models as classes that inherit from
ApplicationRecord. To me, a model is anything that models something.
So a lot of the models I keep in
app/models are just POROs. According to a count I did while writing this post, I have 115 models in
app/models that inherit from
ApplicationRecord and 439 that don’t. So that’s about 20% Active Record models and 80% POROs.
Thanks to the structural devices that Rails provides natively (controllers, models, views, concerns, etc.) combined with the structural devices I’ve imposed myself (namespaces, a custom model structure), I’ve found that most code in my Rails apps can easily be placed in a fitting home.
One exception to this for a long time for me was view-related logic. View-related logic is often too voluminous and detail-oriented to comfortably live in the view, but too tightly coupled with the DOM or other particulars of the view to comfortably live in a model, or anywhere else. The view-related code created a disturbance wherever it lived.
The solution I ultimately settled on for this problem is ViewComponents. In my experience, ViewComponents can provide a tidy way to package up a piece of non-trivial view-related logic in a way that allows me to maintain a consistent level of abstraction in both my views and my models.
The lib folder
I have a rough rule of thumb is that if a piece of code could conceivably be extracted into a gem and used in any application, I put it in
lib. Things that end up in
lib for me include custom form builders, custom API wrappers, custom generators and very general utility classes.
In a post of DHH’s regarding concerns, he says “Concerns are also a helpful way of extracting a slice of model that doesn’t seem part of its essence”. I think that’s a great way to put it and that’s how I use concerns as well.
Like any programming device, concerns can be abused or used poorly. I sometimes come across criticisms of concerns, but to me what’s being criticized is not exactly concerns but bad concerns. If you’re interested in what those criticisms are and how I write concerns, I wrote a post about it here.
I keep my background job workers very thin, just like controllers. It’s my belief that workers shouldn’t do things, they should only call things. Background job workers are a mechanical device, not a code organization device.
Being “the Rails testing guy”, I of course write a lot of tests. I use RSpec not because I necessarily think it’s the best testing framework from a technical perspective but rather just to swim with the current. I practice TDD a lot of the time but not all the time. Most of my tests are model specs and system specs.
Since service objects are apparently so popular these days, I feel compelled to mention that I don’t use service objects. Instead, I use regular old OOP. What a lot of people might model as procedural service object code, I model as declarative objects. I write more about this here.
How I think about Rails code organization in general
I see Rails as an amazingly powerful tool to save me from repetitive work via its conventions. Rails also provides really nice ways of organizing certain aspects of code: controllers, views, ORM, database connections, migrations, and many other things.
At the same time, the benefits that Rails provides have a limit. One you find yourself past that limit (which inevitably does happen if you have a non-trivial application) you either need to provide some structure of your own or you’re likely going to end up with a mess. Specifically, once your model layer grows fairly large, Rails is no longer going to help you very much.
The way I’ve chosen to organize my model code is to use OOP. Object-oriented programming is obviously a huge topic and so I won’t try to convey here what I think OOP is all about. But I think if a Rails developer learns good OOP principles, and applies them to their Rails codebase, specifically in the model layer, then it can go a long way toward keeping a Rails app organized, perhaps more than anything else.