About a year have passed since I’ve been daily using the approach described on my previous post about Clean Architecture in Ruby.
Every time we iterate through it new ideas arise, so here comes a new example step by step to illustrate the improved approach.
A brand new example: transfer money use case
The main use case of our app is described bellow in use case:
<source_account_id>, <destination_account_id>, <amount>Primary course: 1. Client triggers “Transfer Money” with above data. 2. System validates all data. 3. System creates a debit on the source account. 4. System creates a credit on the destination account.Exception Course: Not enough money on the source account. 1. System cancels the transfer.
Create the app
We will call the app (gem) Lannister, because A Lannister always pays his debts.
bundle gem lannister
Transfer money between accounts:Lannister.transfer_money(source_account_id: 1,
Here you can already see a small improvement, we started using Ruby 2 keyword arguments, it makes the parameters clearer (no need to check the original function to see the meanings) and also avoid coupling due to arguments order. You could use a hash, but this would require to fetch it argument inside the hash, not so practical.
The entry point
To declare the entry point of this use case we would normally create a method that calls an internal use case class:
Notice the transaction_handler variable, this is meant to be injected like this:
Lannister.transaction_handler = ActiveRecord::Base
And in runtime:
# use case code
The fact is that we got tired of creating methods only to delegate to an internal class within a transaction. So we built a gem called caze, a DSL to declare use cases, with it the previous code can be replaced by this:
It will also let the transaction_handler accessor ready for you to be injected and will wrap this use case around it. If the use case does not need to be inside a database transaction, just omit the `transaction: true` part.
Transfer money use case
The use case itself contains the domain logic to transfer the money, we need to check if the source account’s balance is enough to make the requested transfer, if positive we create a debit from the source account and a credit to the destination account and respond true, otherwise the transfer is cancelled and false is returned.
Notice that the use case description is part of the file, this can help to understand the goal without getting into implementation details.
The improvement regarding the code itself is the export method which is also part of caze’s DSL. It basically exports the instance method transfer, in other words it creates a class method that instantiates the class with the required arguments and calls the instance method. If the class method must have a different name from the instance method, you can define it with the “as” argument.
This makes it easier to be called from outside as there is no need to instantiate it. It generates something similar to this in runtime:
The entity was a PORO (Plain Old Ruby Object) and still is when we don’t need too many trivial validations:
But we gave up writing manual validations and added ActiveModel:
In-memory repository and an optional replacement
You can define an in-memory repository inside the gem:
One of the cons I previously mentioned was having two repositories implementations: in-memory (for tests) and Active Record (for production); this problem is even more painful when you have do deal with in-memory complex queries, it just felt “wrong”.
To solve it we decided to implement the repositories and Active Record models inside a Rails Engine and use it inside the gem. We chose an engine because it is isolated from the Rails app (delivery mechanism) and will not bloat the gem with a huge dependency. The major concern we had with this approach was speed penalty caused by database I/O, to avoid we configured a SQLite3 in memory adapter for the test suite.
Notice that although the dependency, we are still logically isolated from the database. We can easily replace the repository by another with the same interface. We are just reusing the “real” version to run the tests, avoiding the duplication.
Let’s create the engine, we will name it accounting:
rails plugin new accounting --mountable
The database relationship is very simple:
Here comes the account model:
rails g model account name
And the trade model:
rails g model trade account:references amount:decimal date:date
And now the ActiveRecord repository:
Let’s connect the engine and the gem, open the lannister.gemspec and add:
Gem::Specification.new do |spec|
Since the accounting engine is not published, at Lannister’s Gemfile and add its relative path:
gem ‘accounting’, path: ‘../../engines/accounting’
The relative path to the engine is considering the following structure:
Since we are using a Rails Engine inside the gem, we need to require it inside the main file:
At line 8 we are also replacing the default repository by the one defined inside the engine.
And the last modification is at the spec_helper of Lannister’s gem, we need to configure RSpec to use SQLite3 in memory and migrate every time the spec_helper is called (since it happens in memory it is very fast):
A possible issue with this approach is if you are using some specific database feature that SQLite does not support. In this case you would need to run the tests upon a real database.
Get Balance use case and the CQRS pattern
We’ve started using the pattern CQRS (Command Query Responsibility Segregation). Basically we added a folder called queries at the same level of the use_cases folder, the goal is to separate things that change state (create, update and delete) from read-only stuff.
Within a gem that has a lot of use cases, this proved to be really helpful. Continuing the example, a query will be used to get an account’s current balance:
The repositories described above already have the balance method implemented.
Some conceptual improvements weren’t exemplified, let’s go through them:
A gem for each domain concept
At first we created a core gem that was supposed to contain all the use cases and entities, but soon we realised that this was not going to scale well, so we sliced that gem into several gems with a specific domain concept.
Actually we just created the first two use cases on the core and after that all new concepts were already booted as gems, we’ve created a rake task to bootstrap a private gem with all the configuration needed for our company (gemspec, authors, etc).
The issue that comes with the “A gem for each domain concept” approach is when the same entities are needed in more than one gem. You could duplicate them inside each gem, but the goal of the entities are to be application independent, so we created an entities gem that contains all shared entities that are used by other gems and by the Rails app.
PS: Actually during a talk at Tropical Ruby a new friend told to me that this violates the bounded-context, that is something that I am studying now.
The gem is our core, it represents what the system does (use cases, aka behavior). It depends upon an staple API to fetch data.
The engine in this case represents what the system is (mental model of the user, aka data). It implements the API defined by our core and make database operations.
Rails is the delivery mechanism for the web with its views, presenters and controllers.
Hexagonal Architecture if you will
Another way of seeing it, would be using an hexagon:
Our core is the Lannister gem. The Rails Controllers are adapters for the Web, the Rails Views are adapters for the UI. The Repository is the adapter for the database.
Notice that we are inverting the dependencies. In a Rails Way app the database would be the center, and our domain logic would depend upon it.
By far the improvement that made us faster was regarding the repository being implemented as a Rails Engine, not needing to duplicate it for our complex queries was a good trade off.
When the domain logic is very close to the web, Rails way is more appropriated, creating a layer to isolate something that is already coupled does not make sense. When it is totally disconnected from the web, clean architecture proved to be a good fit for us.
If someone out there is using Clean Architecture with another approach or has some improvements for the approach stated in this article, let’s talk: @fbzga
PS: the slides of my presentation at Tropical Ruby 2015 about this subject are available here: https://speakerdeck.com/bezelga/clean-architecture-in-ruby-tropical-ruby-2015