Working on Green Field projects is often very enjoyable. Such projects come with few encumbrances, and lots of room to explore. They can be really good environments to try out new architectural approaches. I definitely found this to be the case, although I did run into a few issues because of the kind of system I was building.
The project that I applied Clean Architecture to was a microservice for Nav that is meant to allow us to keep track of changes in certain details about a business over time. We provide business health and credit-related services to small businesses and so we end up aggregating a lot of data. The service I needed to build would serve as a repository for certain individual data points over time and allow us to retrieve a snapshot of what we knew about a business based on the data points we had collected. For ease of explanation we will only be concerned with two of the data points that we started with as part of this project’s MVP phase: annual revenue, and founded on date.
In the past I have really liked the approach of Outside-In Development because it elevated the concerns of stakeholders and pushed for understanding of acceptance criteria early in the development process. But, that approach is really difficult to sustain in a microservices environment and even more so when you are only working on one small unit of a larger whole. Clean Architecture provides a nice means to bring something like an Outside-In approach to more seemingly isolated work through the centrality of Use Cases and Entities.
Entities are the central things of a software system, its core nouns. These are the Domain Objects, for those familiar with Domain-Driven Design. Use Cases are the central behaviors of the software system, in some ways they mirror Services in Domain-Driven Design.
Working with Clean Architecture starts by understanding the Entities and Use Cases that your system needs to implement. For this project that started out very simply with the only Entity being the Business Profile and the sole Use Case being to Update a Business Profile. This is an oversimplification though. The Business Profile has certain properties that are its, but because of the variety of data that was conceived of could live as a part of the Business Profile there was also a sub-entity for the Fields it aggregates. So, a Business Profile is an aggregate of its own properties and a collection of Field entities. The sole Use Case was also an oversimplification since an assumption baked in was that updating and creating are synonymous for this service. If an update request is received, and a Business Profile does not already exist, it should be created. This was true because the unique identifier for the Business Profile was not something this service actually needed to own.
Another key concept in Clean Architecture is that of abstracting away technical details. This is accomplished through Gateways, Presenters, and other approaches that allow framework details to be kept wholly distinct from business rules. One of the key examples of this is when doing the translation between Entities and ActiveRecord models. Since all the business critical behavior lives within the Business Profile the ActiveRecord models for the two associated data models are incredibly small.
The only logic that lives in the ActiveRecord models are those things that relate to ensuring the database is kept valid. There is other validation logic that could live here, but it does not. And even the the presence constraint is also present in the Entity for Business Profile, because it is also a constraint at the level of the Domain Model. In this way the Data Model and Domain Model, while related are separate and do not know or interact with each other. Instead there is a Repository, which also goes by the term Gateway in Robert Martin’s writing, that mediates the relationship between these two layers.
The Repository is where all the translation knowledge resides. If ActiveRecord changes its behavior only the Repository needs to be changed. If the shapes of our Entities change, while there may be an impact on the Data Model, that impact is mediated through the Repository.
Interfaces & Testing
Because Clean Architecture introduces additional layers on top of what an MVC framework like Rails already provides there is a need to establish common interfaces for certain parts of the application. The common interfaces are necessary to ensure that dependencies only flow in one direction and to keep certain things like Use Cases from reversing that dependency direction when they need to interact with things like Repositories.
Ruby does not have any native support for Interfaces. While some have used Module mix-ins to mimic Interfaces, I have never liked that approach. I found an approach I preferred was to rely on a feature of RSpec to give me what really mattered, which was assurance of common expected behavior. So, I wrote Shared Examples to validate the minimal behavior of my Interfaces.
Because these tests focus on the common behavior I can easily create a mock repository that meets theses criteria and allows the tests for my Use Case to stay fast with no need to call through ActiveRecord, and by extension also be more isolated.
This was probably the most valuable and widely applicable thing that came out of my experimentation with Clean Architecture in this project. This way of testing interfaces strongly promotes the Liskov Substitution Principle with an explicit focus on the concept of behavioral sub-typing.
Controllers & Presenters
Because what I was writing was a microservice with a JSON API there were other areas where Rails came into play. Request parsing and validation is still an open issue for me and I’m not sure of the best way to handle that, but my Rails controllers act more like I feel Controllers ought to.
The controller actions end up very simple and clear in terms of their behavior. In the case of the show action I call the Repository and pass what it returns to the Presenter which knows how to format the Entity it is given for the way we want to return it via the API. The update and create actions are similarly simple and clear, with their only differentiation coming from the fact that they call the UpdateBusinessProfile Use Case.
Were I to do something different I might let the controller be less DRY and accept that I’m not duplicating any code often enough to justify the way I’ve extracted it. But, that change would be entirely superficial and rooted in preference, and I actually like that my actions have two verbal statements; either load and render, or update and render.
Clean Architecture has a lot to offer. How it pushes for dependencies to only point in one direction is great. The way it separates the Domain Model from the Data Model is incredibly valuable and could make developing applications beyond simple CRUD behavior much better. The other nice thing about Clean Architecture is that it can be applied to web applications, microservices, mobile development, and other arenas; it is nearly universally applicable.
But, if you are working with small services, like I was, it can feel like overkill. My Domain Model and Data Model are really similar. The benefits of Use Cases could be gained in other ways too. So, application scale is a factor that may tip some assessments away from using Clean Architecture. But, that assessment looks very different if the Use Case is complex and thus the clarity afforded by Clean Architecture starts to shine through.
In my next installment I want to explore some speculative ideas that are partially informed by work done by one of my colleagues at Nav. I think the biggest advantages for Clean Architecture may lie in applying it to existing projects as a way of improving them, and even preparing them for Service-Based Architectural approaches.
Interested in reading more about applying Clean Architecture to Ruby on Rails projects? Want something closer to a tutorial, or how-to? I’m working on a book that covers this subject in much greater detail over at LeanPub.
The book is still in the very early stages, but I’m hoping to release it by the middle of 2018, with beta releases starting in January. So, head on over and sign-up to learn more as publication gets closer.