How I organize my Rails apps

by Jason Swett,

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.

15 thoughts on “How I organize my Rails apps

  1. Pingback: How I organize my Rails apps - Sebastian Buza's Blog

  2. Dewayne

    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?

    Reply
  3. Rick Smoke

    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!

    Reply
  4. Olivier

    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.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *