So you’ve watched Uncle Bob’s Architecture the lost years and you are very excited to start using its concepts in real life.
The main idea of Clean Architecture is to isolate your business rules from implementation details like delivery mechanism and database.
For Rails projects, isolating the delivery mechanism seems to pay-off according to my experience using it whenever it makes sense to do so (not for CRUD).
Isolating the database is the hard part, I did it plenty of times and looking in retrospective there were few times that it was worthy, at least in the contexts I’ve applied it.
I’ve written previous articles saying how to implement all the concepts, but this post will focus on the simplest way to get started.
We’re gonna implement a Transfer money use case. The only exception to handle is when there isn’t enough money to transfer.
Let’s start by creating a new Rails app:
$ rails new bank
Uncle Bob said that by using this command you have lost your architecture. Although it might be true, this is the point where most of us are or will start. So this is our real-life starting point and where Rails shines in its convention over configuration.
But there is something we can do to not completely loose the architecture: isolate the core domain into a Ruby module. Let’s do it simply by creating a module in the root of our Rails project:
$ mkdir core
$ touch core.rb
We’ve created a core directory and a core.rb file in the root of the project. This file is just an empty module for now, simple as this:
Next we are going to define the boundaries between the core domain and the delivery mechanism.
Here is how we will call the transfer money use-case:
The input is well defined with Ruby’s keyword arguments.
Let’s also define the output:
=> true # when successfully transferred=> false # when not enough money to transfer
The use case
We are going to create a class with the responsibility to orchestrate the money transfer:
It receives the parameters in the constructor. Inside the public method we will implement the business logic.
In this example we are not going to isolate the database from the domain. We will use the Active Record pattern with the well known ActiveRecord gem that comes with Rails.
So at this point we need to define the domain modeling that will also be the data structure to persist data.
Our domain model is going to have two simple entities: Account and Trade.
$ rails g model account name:string$ rails g model trade account:references amount:decimal
Since we are using Active Record, this also means two simple database tables. An account has many trades.
The amount field from the Trade model indicates if it was a debit or a credit with its math sign: -1000 (debit) or 1000 (credit).
With this we can go back to the use case and implement the main method:
But what if the source account does not have enough money to transfer?
To do this we need to implement another use case, the one that gets the balance from an account.
First the boundaries:
The output is a number:
=> 0 # if the account does not exist or there is no money=> BigDecimal # representing the balance
To find out the balance of a given account we can simply sum all the trades. No need to cache the balance, immutability for the win.
We simply sum the trades amount and voila.
Now we can go back to the use-case and implement the exception course:
Don’t forget the entry point!
We don’t want the delivery mechanism to know about the internals of the core domain. So an entry point is needed.
It can be easily implemented like this:
Catch the train! Using it on Rails
The use-cases are ready to be used in any part of our Rails app. It could be inside the controllers, rake task or a background job.
Since it is not coupled to the web, we benefit from the freedom to use whenever we want.
If we had more than one exception, we could have used ActiveModel inside the use case to respond the errors.
Wanna go all-in?
The approach explained above is very simple. And it partially uses the Clean Architecture concepts. If you want to go all in, there are some other approaches as follows.
Gem as core. In-memory Repository.
In this approach, a gem plays the role of the core domain.
Inside the gem there are entities, they are PORO (Plain Old Ruby Objects), and to access the database it uses Repository Pattern.
Since the gem doesn’t know a thing about ActiveRecord, there is the need to implement an in-memory Repository. You also need to implement a real version of the Repository, that can be done using Active Record.
This approach is explained in depth in this article.
Gem as core. Rails Engine as Repository.
The pain with the Gem as core domain approach is the duplication in repositories: in-memory and real.
After passing through hard times with in-memory repositories (taking more time to implement the in-memory repo than the use-cases), it was clear that another way was needed.
So we’ve came up with a way to re-use the real repository inside the gem.
The idea is to create a Rails Engine, create the models inside this engine. Create a repository and expose the entry points of the repository. This Engine will be used inside the Gem.
This approach was described in this article.
Rails engine as core and data (current favorite)
The same approach described in this article can be applied to a Rails Engine.
With this approach you will have use cases inside the engine, exposed as entry points.
The data will also be inside the engine so there is no need to create repositories. This makes easier to access data without any other abstractions on the top of ActiveRecord.
You will also benefit from the namespace that the Engine naturally creates for you (in models, database tables and use cases).
This is my current favorite approach because it combines the power of Rails with the Clean Architecture structure.
PS: I am writing to explain this more in depth.
Going all-in into Clean Architecture is an investment that depending on the context could be a bad move. Especially the database part, turning it into a detail may cost more than you want to pay, but in other cases may totally pay-off.
If you found yourself creating repositories that only replicate what AR does, that’s a sign that maybe you don’t need it.
Active Record (the pattern) aims to make the domain modeling and database modeling the (almost) same. If you can’t reflect your domain into database, a repository of data-mapper may fit well.
Regarding the isolation from the delivery mechanism (in this case : the Web) proved to be a good way to organize domain business rules in a large scale project.