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.

Brace yourself refactoring is coming

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!

app
├── assets
│   ├── config
│   ├── images
│   └── stylesheets
├── channels
│   └── application_cable
├── controllers
│   └── concerns
├── helpers
├── javascript
│   ├── channels
│   └── packs
├── jobs
├── mailers
├── models
│   └── concerns
├── policies
└── views
└── layouts

It would be a lot better if it could reflect our domains:

domains
├── accounting
│   ├── banks
│   ├── bills
│   │   ├── external
│   │   └── provider
│   ├── bookkeeping
│   └── expenses
├── aggregator
├── finance
│   ├── exchange_rates
│   ├── orders
│   ├── payment_events
│   └── posts
├── hr
│   ├── roles
│   └── vacations
├── onboarding
├── pages
└── production
    ├── capacities
    ├── objectives
    ├── offers
    ├── phases
    ├── project_types
    ├── rentabilities
    ├── tags
    └── time

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

But one does not simply rename its models.

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.

This advice can be automated using a script.

require 'active_support/all'

files = Dir["app/policies/**/*.rb"]
files.each do |filename|
  policy_class_name = filename.split('.').first.split('/').last.classify
  model_class_name = policy_class_name.gsub('Policy', '').singularize
  model_filename = Dir["app/models/**/#{model_class_name.underscore}.rb"].first
  next unless model_filename

  content = File.read(model_filename)
  new_method = "\n  def self.policy_class\n    #{v}\n  end\n"
  regexp_string = "class #{model_class_name}((.|\\n)*?)((\n  def)|(\\nend))"
  regexp = Regexp.new(regexp_string)

  if content.match(regexp)
    content = content.gsub(regexp, "class #{model_class_name}\\1#{new_method}\\3")
    File.write(model_filename, content.split("\n").map(&:rstrip).join("\n") + "\n")
  end
end

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

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”

UPDATE FROM audits SET associated_type = 'Accounting::Bill' WHERE associated_type = 'Bill';
UPDATE FROM audits SET auditable_type = 'Accounting::Bill' WHERE auditable_type = 'Bill';

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.

what did it cost?

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.