For the last 10 years we have built a lot of Rails applications and they’ve grown. The fact is that Rails is a web framework and its foundation is MVC. The MVC pattern was introduced by the norwegian Trygve Reenskaug in the 70's and it was designed for small components and not to be the architecture of a whole app.
By using MVC as the architecture of web apps, everything stays connected (coupling) and makes you go slower because most changes have impact on other parts of the app. Also it kind of “forces” you to think where to put some business logic, should it go into the model, the controller, or the view? Some might tells us to follow the fat model/thin controller, but is the Rails (ActiveRecord) model the right place to add your business logic? An instance of an Active Record model has hundreds of methods and yet you add more methods to it (Single Responsibility Principle anyone?). We have evolved to move business logic to models or services that doesn’t inherit from Active Record, which is already a big win.
Another issue is that our top level structure is screaming the web framework. It is not easy to tell what a Rails app does by looking at the directories structure. You can open a controller file and by following its flow, discover what it does, but it is not intuitive.
Clean Architecture aims to put the Use Cases in the center/top of your structure, so you can easily see what your app does and not how it’s built. Making the web be a detail and not dominate your code. It also makes it easier to adopt changes since it is much more modular and isolates stuff like UI’s, databases, frameworks.
How (the 7 steps)
I watched Uncle Bob’s keynote called Architecture the lost years and it blew my mind. If you haven’t watch it yet, go for it.
After watching it, I was wondering how to apply these concepts to my daily work. So I decided to create a to-do app called bulldoggy to make an experiment, a proof of concept if you will. It worked fine to me, partitioning the business logic (your app) from things like web and databases, make your code focus on what it really does by removing noise.
My approach to apply the concepts of Clean Architecture in Ruby follows these steps:
1) Creating the core gem
I start by creating a gem that will be your app.
I like to use a gem because it has the minimum amount of structure to get going and it does not imposes the architecture, it lets me choose it.
To give a very simple example, we will create the use case add_task of a todo list app, we will call it Tod. So yeah, let’s use bundler (if you don’t have it, you can install it with gem install bundler) to create it:
bundle gem tod
2) README Driven Development
I like to write the README before starting to code, this way I can think as a client how I would like the interface to be. RDD give us this mindset.
So for Tod, we will go with this inside the README:
Adding a task:Tod.add_task(‘write about clean architecture’)
Next step is to write the test of the use case:
We are adding a new task and checking if the Tasks repository’s size was incremented.
When the title is blank, we assert that the Tasks repository size did not change and that the method returned false.
4) The entry point
It is a good practice to have an entry point of the gem, so in most cases the developer that will use it does not need to know about its internals.
You can see that there is a configure method that yields to self and also that repo uses the in memory version if none is provided.
We will use this configure method when connecting to Rails. For now the important part is that the add_task method delegates the “heavy work” to the use case class.
5) The Use Case
The use case itself is where your business logic for this flow would be.
We are instantiating a new Task entity and asking if it is valid individually. If yes then we call the Tasks repository to persist it.
Notice that the task itself does not know how to be persisted.
If the task isn’t valid we return false just as we’ve intent the interface of this use case to be (when you add more validations you can return an array or hash with validations and their messages).
6) The entity
The entity is a PORO (Plain Old Ruby Object) with a simple method to validate itself:
Notice that this validation is individual, if we needed a validation that needs to check other tasks I would add it to the repository and reference it here.
7) Repository (in-memory)
The in-memory repo simulates a simple database and its first version is simple as follows:
At this point you can fire up a console with bundle console and:
Dating Rails instead of getting married
Uncle Bob once said:
being coupled to Rails might be good for DHH, but it might not be good for you.
Let’s create a rails app so it can be used as a plugin for our core app:
rails new tod-rails
Create the rails model:
rails generate model task title:string
To use gem we just created within Rails, you need to create an ActiveRecord version of the repository.
We are encapsulating the Rails model inside the repository, notice that the persist method responds an entity task just like the in-memory version does. It is important to keep the same interface between the repos.
Now it is time to use the configure method of our gem:
At this point when you call the use case, the task will be saved to the database. This can be used in any controller of the Rails app.
The example above is a very simple “Hello World” approach to Clean Architecture. In the real world if I needed to create a todo-list app that would be delivered only thought the web I would not use this approach, the objective of the example is just to give an idea of how we can implement Clean Architecture using Ruby.
I am not telling that this the way to make apps, and it is definitely not. This does not fit well for most web apps, it fits better for apps that have more than one delivery mechanism and have a lot of complex business logic that are isolated from the web. I am also not saying that Rails is wrong and that kind of stuff, Rails is an awesome framework that I love to use but sometimes I prefer to use it only for delivery and data persistence.
It has been a couple of months that we are introducing this approach at Magnetis that has a big size codebase that grows everyday, there are pros and cons as anything that is worth looking at software development. What I can say so far is that the pros are higher than the cons in our context; we also consider the context of the page/feature of the app we are developing, if it is part of the core business we consider using clean architecture otherwise we go with Rails way (there a lot of pages in our app using the Rails way and it works just fine). There are cases that we need to deliver responses not only through the web but also in the command line or for internal API’s, changing the delivery mechanism is easily done with this architecture. We also changed the way data is persisted in the database (twice), so being coupled to the interface of the repository instead of the ActiveRecord model and fields was beneficial.
- let clear what is your app’s intent and not how it’s built, you can easily see what your app does by looking at the use cases folder.
- easily change details like UI, frameworks, database
- tests running faster than ever
- two implementations of the repository (in-memory and the Active Record)
- more files to think about
- over-engineering if you never harvest the benefits.
The context of your business/product/lifecycle is very important here: if you are bootstrapping a startup product or developing the first version of an hypothesis it is more than OK to go the Rails way (it is the way I would go in most cases). As soon you start identifying patterns, it is time to consider a less connected a.k.a modular architecture like the one described in this post or any approach (DCI, Hexagonal Architecture, Service Objects, PORO’s inside Rails models folder, etc) that makes you focus on your business rules.
I’ve written a v2 of this article with lots of improvements:
Clean Architecture in Ruby v2
Almost a year have passed since I’ve been daily using the approach described on my previous post about Clean…
I’ve written a simpler version on how to apply Clean Architecture to a Rails project here: