How I organise my Rails apps

Overview

I've worked in tens of teams, all size of companies, with all kind of colleagues. 

I've mentored a dozen people across my 5 years in web development.

And every time, I find myself explaining how I develop my apps and why; this blog post tries to summarise it in a condensed way with only three thousand words ! 

It is not meant to be a source of absolute truth, and sure there are a lot to say in return. I would love to hear your comments, but please consider that exposing a point of view is always subjective, biased and incomplete. In honesty, I wish I will look down this article in two years and grind my teeth with you, until then this is for now what I consider the best way to develop Rails apps.

Summary of the topics 

  • Models
  • Views
  • Controllers
  • Namespaces
  • The lib folder
  • Concerns
  • Background Jobs
  • Tests
  • Service Objects
  • Rails code organisation in general
  • Conclusion

Models

What is a model to you ? Is it a class that maps a table through ActiveRecord ? Is it a class that contains all the intelligence around a business entity ? 

What weirds me every time I hears this answers, is how this is so Rails (and web) specific. When developing a Rails app, I'm indeed using a web framework but only as a tool; like any other software I will need to model entities but it has nothing to do with a database table. If you develop let's say a video game with characters and NPCs, you will need to model these entities no matter how; that's how you do OOP and if you're here today, you sure love OOP because Ruby is the king of all on this topic !

So, the models folders is to me the place where I should put my classes that model my entities. This of course means putting ActiveRecords models, but also all the classes around them: a specific Config, an Event, a logger

We will speak about it in the Namespace section, but I also group as much as possible my entities in subfolders in a common namespace (as per Zeitwerk philosophy). So if I have a Quest model, then you will most likely find something like Quest::Event, Quest::Definition, Quest::Reward in my code base.

The take here is that you should model all your entities, anything that can be named should have a dedicated model. Then, try on keeping them small and focused on themselves; this is the best way to keep a clean architecture and an immutable model base. Business logic will move, how you represent your entity will move accordingly, but updating a model should not impact any other model. 

Views

Views is a hard topic, because it varies so much ! For instance an API Rails app won't have many views, on the other hand a monolith could use an embed React in a javascript folder, an old-school Rails ERB architecture, a list of ViewComponents or plain Hotwire code. 

To avoid details about each ways, I will just say you that as for everything, I tend to keep my files tiny, my folders grouped by sens and the visual entities isolated from each other. 

For instance, if a component needs data from an API to render a list of widgets; I will not have a single file for it; and neither should you ! Instead I will have the following organisation:

  • a plain old javascript file for the API that handles how to reach the right API (environment specific, authentication third party etc);
  • a plain old javascript file dedicated to host the semantically close calls to that API (for instances DashboardWidgets);
  • a view specific container file that handles the call to the api, the loading and returns it to the actual component;
  • a view specific component per widget that will only accept its required data as props and would broadcast user inputs only through events and callbacks.

What is important here is that every file handles one and only one specific thing, this way I am sure that changing any behaviour inside a file will not break anything as long as I am not changing how they communicate with other files. This means taking good care of when you change your interface (how you use your file), what you export, how you name them (this should be pretty permanent). A file interface should not move a lot during its lifetime; it is what I call abstract, versus how it works that is implementation specific. The former want to stay the same, the latter will move no matter what you want. 

Controllers

To grasp why I organise my controllers the way I do, we need to define what is a controller as a core concept. 

Imagine being the core member of the Rails development team; you have plenty of work. Your team have implemented the Request & Response models and you are tasked to handle requests. All you know is that you will receive a request and need to respond. 

Of course you could add a RequestHandler model, that will call a ResponseFactory service; or create a RequestAnswerable concern - and this will be close to how a web developer thinks nowadays - but this is not how you approach this. Instead, you create a new brick, a new block that is dedicated to answer requests giving as much control as you can to the end user, the developer. 

For this, you create a Controller, something that can be called, that is request independent, thread isolated, can embark some logic and respond perfectly to any request. You push your work, merge it and take a look at your users, knowing they will embrace their new tool. How sad can one, seeing every day a junior putting everything in a controller, and copy pasting the same code for every action of every controller.

In my career, I have seen thousands line controllers, almost 90% duplicated code controllers and clean controllers. I cannot express the joy I felt the first time I saw a controller with no duplicated code, being 100 LOC long and doing exactly what it is supposed to do. This is what controllers are for: receiving params & headers, calling something blindly and rendering a respond based on the return of the call. That's it, nothing else. 

By its very nature and definition, a controller can mostly be abstracted: you can (and should) build a ResourceController, or a generic implementation inside ApplicationController that does all the boilerplate for you; and in the specific controllers only override the necessary. For instance, a basic create call can be abstracted this way:

def create
    @resource ||= resource_class.new(permitted_attributes(resource_class.new))
    authorize @resource
    @resource.save
    expose @resource, status: :created
end

Once you have an abstract way of handling requests, you can add more hooks, callbacks and do your thing; your creativity is the limit, and the controller are forced to stay small.

Namespaces

Namespaces in Ruby is a bit different from namespace in other languages. For instance, in C++ you would define a namespace explicitly by its name, putting the content inside a bracket block. In Ruby, as everything is an object, namespace are too, and they are modules. This means they can be inspected, augmented, inherited; there are infinite possibilities of dark magic with modules, but let's focus on their namespace aspect.

By defining things inside a module, you are namespacing it to the module. So this

    module Thing
      CONSTANT = 'duck'
    end

will namespace the constant CONSTANT to the namespace Thing. 

This has two major benefits:

  1. You avoid name conflicts. How come you rarely encounter an issue of constant already defined when adding tens of gems in your application around web development ? Mostly because a gem must namespace all its content inside a root class (or module); that's why your Name class that handles the formatting of a name does not conflict with Name class of Faker that handles the generation of credible names: one is actually ::Name and this other Faker::Name.
  2. You can seamlessly (and with priority over others) access your namespace constants: the Faker gem developers does not bother writing Faker::Name every time they use it; Ruby bubbles up the lookup for constants and reading Name, it will ask the current namespace before asking others if such constant exists.  

With this said, we can now focus on why namespaces should be used: group semantically close concepts inside a common box and that can interact easily. 

In a Rails application, this can have tons of usage. One example I've already mentioned is the separation of entities in my models; with a Quest having a Quest::Condition, Quest::Event and Quest::Reward; Quest might not be a module but by reopening the class you can define scoped constants; and this will allow the Quest class to access Event, Condition and Reward easily without bothering where they are. In addition, the Quest::Reward that handles "Is this quest rewarding this user" question does not conflict with the Reward model that handle the actual reward definition.

This workS also with non model files: you can namespace view components, lib classes, service objects, config file, seeding files, even controllers !

The lib folder

Have you ever written code that should not be used often, maybe in one or two very specific cases ? Maybe an helper for one action of one controller with a third party in a specific error case ? Have you ever written code so generic and clean that it could fit into any website ? Or written code that is only used for deployment and you feel like it should not be a model nor a service ? 

Semantically the lib folder is meant to store files that should not be auto-loaded, but instead required only when necessary. They are also meant to be quite far from your business logic, unique tools that can help in precise cases; no wonder why the rake tasks are in lib by default !

In my lib folder, you can find my tasks obviously, but also deployment scripts, some monkey patching and specific tools that I mostly not use (for instance a rate converter).

What matters, is that the moment you need this files, you remember to require them; ruby developers have tons of require calls in their files, but Rails developers almost never write them !

Concerns

I use concerns in two scenarios : 

  1. I want to lighten a file, grouping in a concern all the LOC around a specific topic
  2. I want to abstract a specific topic, to use it in several files

For the first scenario, imagine yourself editing the User model. It usually grows terribly wrong: this class is core to the app, will grow for every other feature and be referenced everywhere. Well, instead of having a 2k LOC User class; you can have a 30 LOC User class that includes 20 concerns. Here is an example of my Account (ie. User) class in an old project of mine (quite close to a video game).

    
class Account < ApplicationRecord
  include HasDevise
  include HasNotifications
  include HasItems
  include HasItemTransactions
  include HasItemShop
  include HasDevelopmentEasterEggs
  include HasCaveExploration
  include HasAbility
  include HasStoryTelling
  include HasFriends
  include HasQuests
end

See how this is readable, clear and concise ? This approach sure has benefits but also some drawbacks. For instance, finding the  receive_notification method can be hard, but with LSP & modern IDE, this should not be too hard. Moreover, as long as you name semantically well your concerns, you'll find your way into what you are looking for quite easily.

The other use case for concerns is to abstract logic; for instance Archivable model. Bonus point if you naively suffix your logic by -able; such behaviour totally fits into a concern. This way, you can group in a single file how to archive and distribute it in every archivable models.

module Archivable
  extend ActiveSupport::Concern

  included do
    scope :archived, -> { where(archived_at: nil..DateTime.current) }
    scope :not_archived, -> { where(archived_at: nil) }
  end

  def archive!
    update(archived_at: Time.current)
  end

  def unarchive!
    update(archived_at: nil)
  end

  def archived?
    !archived_at.nil?
  end
end


NB: You may have noted that I name my concerns a bit differently that what is advised; usually interfaces in computer science (what concerns partially answer) are named with a -able suffix. I find it a bit hard sometime to find the right word to "able", and like to separate interface concerns (that I try to suffix) from compartmentalising concerns (that "Has" something) like this. Another convention is to suffix by Concern the concerns; I simply dislike naming something by its programming nature instead of what it represents.

NB2: Worth noting, a concern is simply a plain old ruby module with extra class methods; the ruby developer does not have concern in its toolbox, but does abuse of modules. If you don't need the class_method or included blocks in your concern, you might want to use a plain module.

Background Jobs

This section will be short; even though it will deserve its own article in the future. Background job is the tool invented for background processing. Exactly like Controllers, Background Job are only there to solve a technical issue (background processing), and have one responsibility (tell if it fails). 

Therefore, it is no place for business logic; exactly like controllers it is a place where you call methods; and raise errors in case of issue.

So everything I said about controllers is true here: keep them short, don't bring logic in here, call your services or models or whatever and focus on error handling !

    
module Namespace
  class ThingJob
    include Provider::Worker
    
    retry_on ActiveRecord::RecordNotFound, attempts: 3

    def perform(model_id)
      model = Model.find(model_id)

      if model.condition?
        Service::DoSomething.new(model:).call
      else
        Service::DoSomethingElse.new(model:).call
      end

    rescue StandardError => e
      raise ActiveRecord::RecordNotFound # for the sake of example
    end
  end
end

    

Tests

I'm not sure I would have enough time to talk about all the things I want regarding tests in my life. More articles will come about them, but what I can summarise is the following:

  • Test files can be quite long, even though it should not;
  • The tests themselves should be the shortest possible;
  • It is rarely worth abstracting tests, as the moment your tested classes diverge you'll have to rewrite everything;
  • Use abstract test cases (like shared_example in rspec) to save you some time and benefit from mutualisation;
  • Namespacing your tests is critical. In rspec, this means using lots of describe & contexts;
  • Learn your test framework because there are actually tens of experts working on them and they already solved all your problems in advance, you just don't know how;
  • Do not test implementation, test the end results and over all your interfaces;
  • Do not test your framework or your gems;
  • When creating something new, explore, write code, delete code then freeze everything with tests;
  • When updating existing code, test the wanted behaviour then code it (TDD).

I exclusively test with rspec; even though I love that Ruby as a test framework built-in, I find rspec's DSL perfect and it allows me to express myself just like a want.

My test files are organised as follows : 

  • Every semantic entity is unit tested;
  • Every test is grouped inside a folder (this can be a rails folder like models, but a semantic folder like graphql), and deeper levels are welcome;
  • Every API endpoint is fully tested from End to End to freeze their interface and ensure their behaviour matches the intent.

Service Objects

I'm not a huge fan of service objects. I used to be though, because it solves one of my core problem in the web development: where do I put business logic; heavy treatment, etc.

Putting everything in the controller is level 0, juniors don't care about architecture and maintainability because they focus on making things work.

Putting everything in the model is Level 1, you understand that it is not the controller duty and your read somewhere that models is a good place so here you go.

Now I'd say service object is Level 2; it answers the separation of concerns. Don't read me wrong; it is a proper solution that is viable and scalable ! My issue with service objects is when they become the go-to options, becoming an anti-pattern. When you thing "business logic = service object" I disagree. In theory, Service Objects should have one entry point (usually the instantiation) and one exit point (usually a method like call or process). Their meaning is to encapsulate a process (called a service) inside one single object. They are not meant to grow, nor to have several exit points. 

The moment your service object has several initialisers, a static method to build it, several methods exposed; it indicates the need to represent an entity and its behaviour. Did you read this ? "to represent an entity and its behaviour"; this is - in my humble opinion - the last level (well for now!): everything should be models. Small, numerous, sometimes complexe models.

If you are tasked to write the purchase of an item; sure you can create a PurchaseItemService; but why not create simply ... a Purchase class ? Even an abstract one for your interface, and an explicit one for your PSP (stripe, checkout or whatever).

Obviously you can't model everything, and sometime you have a flow, a complexe procedure with several steps where service objects are confortable (even though you could create a Procedure object, with steps that get specialised) and that's OK!  

In the end, as long as the final files are small, not so versatile, easy to find and scalable I'm fine with both approaches.

Conclusion

Well if there is still someone at this point of the article, congratulations you survived an long monologue of a frustrated rails developer that mentored too many juniors recently ! 

As you may have understood at this point, there are several points that appear in all the above sections:

  • Small files
  • That do one thing
  • That do not change their interface
  • But allows to change their implementation
  • Inside an abstract architecture where no one knows anyone

This is the lessons I've learned during my time in agency and as a freelancer; and have been conceptualised by two aspects : 

SOLID principles

  • Single Responsibility Principle: a class must do exactly one thing 
    • This goes by small but numerous files
  • Open-Closed Principle : A class must be open to extension but closed to modification
    • This goes by freezing the exposed methods but letting the implementation open
  • Liskov Substitution Principle : You can manipulate a class and all its children transparently
    • This means you should not check for a variable class to call a method
  • Interface Segregation Principle: A class should have lots of small interfaces that can be composed than bigger ones that can't
    • This goes by inclusion of small modules and concerns
  •  Dependency Inversion Principle: high level modules should not depend upon low level modules. Both should depend on abstractions.
    • This principle is quite hard in ruby. When possible, instead of hardcoding which service you are calling, pass it in (optional) parameter so that the caller can changer which dependency they want.

I will detail how you can be more solid in ruby in a future article ! 

Clean Architecture

This one also deserves its own article, because I've read the book twice already and still can't grasp half of its knowledge. But of all the things I've learned from it, is that architecture is global; it is not a matter of third parties, of ops or gems. It's the same principles, applied to all the layers, of all sizes !

  • When you are developing a huge payment module for a big company, you will have to provide a guidebook on how to use it, make sure the abstractions are usable by the other developers and that every brick is clean (as of SOLID). 
  • When you develop a small feature for a startup; you should also provide a guidebook on how to use it, make abstraction and be sure that every components is clean. 
  • When you develop a new method for a class; you should also provide a proper prototype on how yo use it, abstract all dependency and be sure that your code is clean.

In the end, Clean Code, Clean Architecture; it is always the same: make everything small, composable, tested, readable, maintainable at all levels; this is how I organise my Rails app.