On the team I work with at Nav we’ve got three different projects running that have all been experimenting with Clean Architecture. My project in particular is a green field effort where Ruby on Rails was chosen as the starting point and applying Clean Architecture in this context has been really instructive. Many of the lofty promises of Clean Architecture have ended up seeming incredibly viable, and I’m intrigued by the possibilities of applying Clean Architecture to existing Rails projects as a follow up.
What is Clean Architecture (Briefly)
Clean Architecture is an approach to application development that seeks to isolate business logic cleanly from technical details like platform (web, mobile, etc), persistence (RDBMS, NoSQL, etc), and externalities (messaging, job queues, etc). Another key aim of Clean Architecture is to control both the flow of control and the direction of dependency in order to achieve the desired separation of business rules and technology details, as well as to ensure robust, and fast testability of all an application’s layers. In many ways Clean Architecture feels like a refinement of Domain-Driven Design, but that is an oversimplification.
In the typical Ruby on Rails application the models are the focal point for a lot of development, both when a project is starting out and as a project matures. This makes sense since there is a strong tendency in Rails to think of the data model and the domain model as the same thing, which I believe is a smell. At the heart of an application implementing Clean Architecture are the Entities. These represent the essential nouns of the domain model along with their necessary business logic.
Entities don’t know about persistence, or presentation. They just know about their data and their essential behavior. In applying Clean Architecture to a new Rails project I started out by defining my Entities as OpenStructs. This provided me with a lot of initial flexibility, especially in not having to explicitly define the attributes I needed. This flexibility allowed me to gain a better understanding of how the central Entities of my service needed to work before actually having to implement them and potentially deal with awkward refactors. At the same time I defined my data model via very simple ActiveRecord classes that only implemented the barest of validations and associational declarations. What I was missing was validation of the data going into certain fields on my first Entities.
I also built the initial repository interfaces to allow my Entities to be retrieved and persisted via the ActiveRecord classes. I kept the surface area of these repositories as small as possible, not only because my application is very focussed in its purpose, but to make as few assumptions early on in the project as possible. But, I will dig more into repositories in the final section on Adapting to Rails.
As I began to more fully define my Use Cases I refined the Entities and moved away from simple OpenStruct implementations to purpose-built classes. In the service I am building this resulted in the creation of two classes initially, one defining the root Entity that my service was concerned with, along with a specialized class (now multiple classes) that represented certain fields on that root Entity which required special validation behavior. At this stage I pulled some validation logic from my ActiveRecord classes into the specialized field class I had created as an acknowledgement that those validations represented essential business rules that the Entities should be responsible for.
Now that the service is at the stage of minimum viability I can say that I really like the clear separation between Entities and my ActiveRecord classes. By keeping essential business logic out of ActiveRecord, my domain model is fully framework agnostic. With the way I have implemented repositories in the application the things that would need to change were I to switch away from Rails, or even just away from ActiveRecord have been completely contained. The only leak I have in the system right now is around some of my integration tests, which use the convenience of ActiveRecord to keep those tests concise and easy to understand.
As the layer diagram above illustrates, the next substantial piece of Clean Architecture comes in the form of Use Cases. These are specifically meant to encapsulate the business logic that is unique to the application and its functional domain. Compared to the model above I flattened things out a bit with my Rails controller methods instantiating the necessary Use Case, passing it lightly processed request state details. I also allowed the controller to instantiate the relevant presenter with the results of the Use Case.
So far, I believe this flatter approach lowers the friction of implementing Clean Architecture within a new Rails project. However, I am working through the criteria that would lead me to introduce more formal boundaries between my controller methods and the Use Cases. I’m hoping that discussions with other team members that are also experimenting with Clean Architecture can help shape those criteria.
My service only has a single Use Case of any consequence right now and while I am suspicious that it currently knows a bit too much, it does not have any dependencies on Rails, which makes it portable, along with the Entities. This portability has a lot of appeal for me and seems like a logical extension of the ideas that run throughout Domain-Driven Design.
Adapting to Rails
So, Entities and Use Cases ended up with a high degree of isolation from Rails and all that it provides. But, there were touch points that were necessary to integrate these well encapsulated units with the framework and other tooling, including a message service. The three areas that acted as adapters for these technical details were the presenters, eventers, and repositories.
The presenters did what might be expected, they knew how to present an Entity in a specific format. In the case of this service, that format was JSON API. The eventers served as the gateways to send messages out on through the message service. And, the repositories mediated the relationship between Entities and the persistence layer, ActiveRecord. These adapters keep changes localized for the technical details of presentation for the RESTful API, persistence, and eventing. One piece that I am considering introducing to further this adapter abstraction is a consumer, which would handle parsing API requests into the necessary structure that the Use Cases need to act. I am still contemplating at what point this additional abstraction makes sense.
So far I think that Bob Martin’s ambitious goal of isolating business logic from technical details like frameworks, and persistence, is definitely achievable. And, the cost does not seem especially high. I also think the costs for adopting Clean Architecture within a Rails code base appears much lower than in languages like Java. Some other effects of Clean Architecture is that all of my code has become more clearly testable. Test-Driven Development came very easily, and clear boundaries made it straightforward to provide sensible doubles that allowed tests to run very fast. The project currently stands at a measly 389 lines of application code, with a test to code ratio of 2.5 to 1, and the 248 tests currently run in just over 4.1 seconds.
In my next installment of Applying Clean Architecture I will delve into some specific things I learned about testing for classes with common interfaces and some practices I think have broader applicability. This will also mean some actual code examples to help better illustrate some details of implementing Clean Architecture with Ruby on Rails. And, after that I want to explore ideas relating to how I believe Clean Architecture can be effectively applied to established (or legacy) projects.
Want to learn more about Software Architecture? Join me & other industry leaders at the O’Reilly Software Architecture Conference and help engineer the future of software! Software Architecture happens February 25–28 in New York. Register now and get the best price—ends December 1. Save an additional 20% on your pass by using discount code FRIEND. Check out the full schedule or go read about my talk on Building a Technical Coaching Program.