Overview
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.
- Controllers
- Namespaces
- Models
- ViewComponents
- The lib folder
- Concerns
- Background jobs
- JavaScript
- Tests
- Service objects
- How I think about Rails code organization in general
Let’s start with controllers.
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 CustomersController
.
The problem
Controllers can start to get nasty when there get to be too many “custom” actions beyond the seven RESTful actions of index
, new
, create
, edit
, update
, show
and destroy
.
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 new_message
and 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.
The solution
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#new_message
and CustomersController#create_message
actions can become CustomerMessagesController#new
and 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 Customer
or Message
with this stuff which is really not all that relevant to either of those classes. I might put a create
or 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.
Namespaces
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 Billing
and Schedule
namespaces and rightly assume that when they’re thinking about one, they can mostly ignore the other.
Contexts
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::ByDayCalendar::AppointmentsController
and 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.
Models
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.
ViewComponents
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.
Concerns
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.
Background jobs
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.
JavaScript
I use JavaScript as little as possible. Not because I particularly have anything against JavaScript, but because the less “dynamic” an application is, and the fewer technologies it involves, the easier I find it to understand.
When I do write JavaScript, I use a lot of POJOs (plain old JavaScript objects). I use Stimulus to help keep things organized. To test my JavaScript code, I exercise it using system specs. The way I see it, it’s immaterial from a testing perspective whether I implement my features using JavaScript or Ruby. System specs can exercise it all just fine.
Tests
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.
If you’re curious to learn more about how I do testing, you can read my many Rails testing articles or check out my Rails testing book.
Service objects
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.
What folder do you put the CustomerMessage PORO file in?
app/models
Great article!
Thanks Jason
Thanks!
Glad you’re back!
Jason, this is great! I’m writing an app from scratch now so it’s a great opportunity to be very careful with the structure of it so that the app will be easy to maintain in the future. Appreciate the post!
Thanks, glad you found it useful!
Pingback: How I organize my Rails apps - Sebastian Buza's Blog
How do you feel about abusing Rails through the use of “require_dependency” to ensure that your ad-hoc directory/file naming convention does not mess with the auto-loaders?
No opinion, sorry. Never heard of require_dependency before now.
I shared this with my team so they can a ‘Seasoned view’ of what good organization in a Rails app looks like. I am interested in pursuing your use of non-ActiveRecord models and PORO use in lieu of the standard Rails Model. I like your ideas behind namespacing too!
Thanks! If you end up using any of these ideas I’d love to hear how they worked out for you.
Since you seem to have a lot of classes inside your app/models I was wondering if you also apply the context pattern you’re using with your controllers with those.
If you’re asking if I organize my models using namespaces, yes!
Where do you put query objects?
Hopefully this answers that question: https://www.codewithjason.com/organizing-rails-files-by-meaning/
how about third party API’s? how to implement an external access without to be coupled?