Rails apps have layers but no modules
… and check why 5600+ Rails engineers read also this
Rails apps have layers but no modules
You can have 200 models and zero modules. That’s the problem with typical Rails conventions. Rails supports layers - models, views, controllers. But layers are not modules. Within one layer - especially models - usually all is mixed together. There are no boundaries.
Order.first.user.invoices.last.line_items
Such code is not so uncommon. It crosses 4 business boundaries. In just 1 line of code. All thanks to associations.
The problem with associations
One of the first thing we teach in Rails is associations.
class Order < ApplicationRecord
belongs_to :user
end
It’s very readable, feels right. Allows us to call it like this:
Order.first.user
Then we have the User class:
class User < ApplicationRecord
has_many :orders
has_many :invoices
end
and the Invoice class:
class Invoice < ApplicationRecord
belongs_to :order
has_many :line_items
end
and this is how we allow the original code:
Order.first.user.invoices.last.line_items
This is how we boil the frog. One step at a time. One column at a time. One association at a time. The result? A User class with 100 columns in the database.
DRY and god models
There is a misconception about DRY - Don’t Repeat Yourself. We have an existing User class. It feels right to just add things there. No one was ever fired for adding a new column to the users table. It feels like the User class is the right abstraction for DRY. Yet, it always ends as the god model.
Service Objects don’t help with modularisation
Many Rails teams believe that Service Objects are the solution. They are, but to a different problem.
Service objects help us when our controllers become too big. They are called from the controllers and they are the ones orchestrating ActiveRecord models. Often they handle transactions too.
What is good about them? They are creating a boundary between the HTTP layer (controllers) and the domain layer. They also are a good solution to the transaction boundary.
Service objects are a new layer. We could now call it MVCS. Model View Controller Service. It’s not bad. It does help with unit testing - it’s easier to unit test a service object than a controller action.
Service objects do nothing about modularisation. They don’t create new boundaries. They don’t help with composing modules. Service objects are just another horizontal slice.
Microservices
It’s usually around this phase in the architecture - MVCS - when a decision is made. We will go microservices.
Sometimes it comes from the team itself - what can be a stronger boundary than a network? The team hopes it will enforce a better design. Microservices bring the hope of starting fresh — new language, new design, better boundaries. But the boundaries still aren’t modules.
Are microservices helping with the modularisation? Nope. They are just yet another horizontal layer. This time we add a layer behind a network call. We no longer have transactions, it’s harder to run tests, the build takes longer. All for the benefit of having 3 new Go microservices and adding new layers of serialisation/deserialisation. More layers, less performance, but still no modules.
A bitter conclusion
Rails makes it easy to add code. It doesn’t make it easy to isolate it. 200 models. Five layers. Zero modules. That’s the default.
In 1972, Parnas wrote that a module hides a design decision from the rest of the system. Fifty years later, Rails apps hide nothing. What does your User class hide?