Use domains with Rails
In 2011, we started an internal Rails project called the Hub. At the beginning, it was as a side project set up to help the company on time tracking, finance, and projects progress. It grows organically with us, with Rails and with our experience. In 2018, the state of this project was bad. Backends developers had fear to work on this project: it was one of our biggest Rails project, it had untested code, strange patterns and the functional documentation was in the head of our product owner. Luckily, this project is also a key instrument for our financial team and they decided to invest in a complete UX/UI redesign. We used this opportunity to split the front end and the back end by designing a REST API and a React SPA Front web. It was time for some big refactoring.
Domain Driven Design
The main fear of the developers to work on this project was that they could update some part of the code and break something else. We heard about DDD and though it could help solve our problem by creating boundaries between domains: therefore, when we update some part of the application we cannot break unrelated features. We also started to use interactors so that it is harder to miss a side effect.
During the redesign process, we set up an Ubiquitous Language. It helped us identify how our Rails models should be named and avoid the translation from the stakes holders to the developers by the product owner.
Once our domains were identified, it was time to reflect our choices in the code. One solution seems obvious: namespaces. It was also time we addressed another problem: the screaming architecture. If you look into our project, it currently screams Rails!
It would be a lot better if it could reflect our domains:
By showing these domains, you have some insight about what our Hub project is. Maybe you can guess why it was important for our financial team. It is not perfect or even sufficient to understand everything but is definitely a lot better than the rails directories. How can we implement this structure?
Refactoring
.
The first step is to make sure that what you will rename or move is covered by your tests. If it is not, you can stop right here and write the tests to ensure the code is covered by your tests suits. Tests are your safety net during any refactoring steps.
Once we have a test for each component of our application, we create a directory domains
in our app
folder. We will move incrementally all the files in this folder.
Because Rails is using Convention over Configuration, there are implicit dependencies. In order to perform the refactoring step by step,
these dependencies have to be explicit.
We specify the dependencies so that we can perform a search and replace to add a namespace or to rename the model.
- If you are using draper, set the
self.decorator_class
method in your model - If you are using pundit, set the
self.policy_class
method in your model - If you are using factory bot, set the
class: MyClass
argument in your factory - If you are using helpers, call
helper MyHelper
in your controller. - If you are using string names to infer classes like
"xxxx".tableize.constantize
, refactor this in a method like pundit and draper that can be overridden.
This advice can be automated using a script.
Then, we can rename one file at a time, run our tests (or let our CI run the tests), commit and proceed to the next file. From experience, it is better to start with
- Decorators and Policies
- Helpers: you need to make sure that
helper Namespace::MyHelper
is called in your controller. - Controller: you should use
scope module: "namespace"
in yourroutes.rb
to avoid changing your urls - Model
Models are the trickiest part. You need to write a migration to rename the table or you will need to use
self.table_name
and makes sure that references to the table are not updated in your queries. You need to
specify class_name: Namespace::MyModel
in your belongs_to
and has_many
. If the model is referenced by a
polymorphic relationship or uses STI,
you need to write a migration to update the name of the model in your database. For example, if your model
Bill
is referenced by Audited::Audit from the gem audited that uses polymorphic relationship, you will need to perform the following SQL to add a namespace “Accounting”
Renaming one file at a time is for security. By doing this way, you are guaranteed that your refactoring will succeed and you will get back to a safe place where all your tests are green. You can rename everything at once, it is faster but the number of changes to perform can be daunting and you can be stuck in a place where your tests are failing and cannot see the end of the tunnel.
Splitting an existing Rails application into domains is consuming a lot of time. In our case, we were splitting from a Rails serving html content to a REST API, a large portion of the code were no longer used and we could use this cleaning process to perform the refactoring. Otherwise we would have not done it, it was not worth it. At the end of our refactoring process 2287 files were updated (about 60 controllers were completely rewritten).
Since then, we are using namespace approach in all of our starting projects. We don’t need to wait that our application
grew out of control. It can be hard to find different domains in a new app, but it is OK to start with only one domain. A good rule
of thumb if you want to split your files in subdirectory is to use the plural of your models as the directory/namespace name. By using this rule,
we found that some models come naturally in some existing subdirectory. For example if you have a new Category
model to categorize Post, it makes perfect sense
to create the Category
in the Finance::Posts
namespace (in the finance/posts/
directory)
Conclusion
We were drawn by DDD to redesign our app and it helps us. But in fact, we only use the Ubiquitous Language. Unfortunately, I can spoil you since we are now in 2021, there are still some translations occurring: for example stakes holders are now calling a Career Path what we used to call a Post. Since the features did not change, no time was allowed to rename the models.
We recognize we are not DDD experts, we only touch the surface. We did not use the Aggregate pattern (at least not on purpose). What we learn is how our project can be a Rails project and screams what it does. Having namespaces helps lower the entry barrier and is not costly even if it breaks some convention over configuration principle.