The evolution of “Code Cohesion” and “Separation of Concerns”
The software industry has recognized the values of “Separation of Concerns” and “Code Cohesion” for more than two decades. Many articles, books and software-thinkers have contributed methodologies to implement these important values.
In this short article I’d like to compare the conservative “Layers” solution with a new approach, which I call “Microunits”, that can open new ways of solving software design challenges.
״Layers״ in a Nutshell
Layering different areas of code is an old and very good way to implement the “Separation of Concerns” value we all love. This technique usually consists of splitting the code functionality in different layers where, in the most simplistic version, we have the UI Layer (where applicable), Business Logic Layer and the Data Access Layer. On top of these, developers frequently add other layers depending on their specific needs. For example, a Common Layer can be used to “hold” cross layers logic and DTOs, or a Controllers Layer can be introduced to handle the exposed REST APIs logic, and so on.
This technique has always worked – and still works well – because it enables us to separate different core capabilities and make sure that when we introduce code changes in one layer – the Data Access layer, for example – it results in low to no impact on the Business Logic Layer and vice versa. As long as the layers respect the exposed contracts, other layers are relatively safe.
Let’s talk about a concrete example. In the diagram below we have a simple layered service that implements some banking capabilities. Let’s call it the “BankMe App”.
Banking App is composed of some Microservices, and a bunch of monoliths.
One of the Microservices is the “Banking Microservice” in charge of all activities related to the customer’s balance: Withdraw and deposit money and get balance:
Banking Service’s business implementation (Deposit, Withdraw, Get Balance) is split into different layers which provides both “Separation of Concerns” and a nice “Code Cohesion” between the different parts of the implementation.
However, since I first started working with this paradigm 15 years ago, I felt that something was missing, although I wasn’t able to put my finger on what bothered me.
A few years ago, I finally got it. The “Cohesion” and the “Separation of Concerns” supplied by the Layer paradigm are based on blocks that gather very generic capabilities: Data Access, Business Logic, and so on. However, the heart of BankMe’s business implementation is scattered between these layers, so whenever we have a feature request related to transferring money, we need to touch all the layers (packages/jars/dlls) from left to right.
This breaks the cohesion, as each layer consists of many different features: Deposit, Withdraw, Get Balance, and so on. Touching all the layers can make the application more fragile, touching one feature can break the entire application.
Obviously, these kinds of issues can be addressed by using different Design Patterns, isolating the different features in each layer and writing “defensively” in order to prevent code fragility.
But something still felt wrong. Because the Layers technique has an inherent flaw. It splits the knowledge of the various features (Deposit, Withdraw etc) between different packages/dlls/jars, meaning the solution isn’t so cohesive after all.
Rigid Code Path
Another aspect that bothered me was that the Layers technique introduced a very rigid Code Path. The Code Path always moves in one direction in a “Cascading Stack” style. One method calls a second method which calls another method, until we get to the Data Access Layer, and then the code jumps back in the stack until the stack’s root.
This rigid one-way Code Path doesn’t allow us to implement more sophisticated solutions. For example, once the stack hits the Business Logic layer, you can’t point back to the layer above (Cyclic Dependency) and then back to the Data Access Layer.
Until a few years ago, I wasn’t able to find a good replacement for the Layers technique. Ultimately, I didn’t want to lose its benefits, even though they aren’t perfect. And then it hit me.
But, before we jump to a possible solution, I want to give you some more context.
Today the Microservice Pattern is well known and covered in a tremendous amount of books, so I’m only going to cover the basics.
The main idea of microservices is to split a solution’s features into dedicated services that are designed to “do one thing well”.
These services then expose APIs to other services so that each can complete its dedicated task by invoking the methods of other services.
Let’s examine our BankMe Application and zoom out from the Banking Service implementation.
In this illustration, we have three microservices that encapsulate their business logic and communicate remotely via sync or async communication protocols.
There are many benefits to such a design, but the ones relevant to this article are: “Business Logic Cohesion” and “Independency” between the different services.
The services are self-contained and the logic doesn’t leak across other services. Everything about transferring and withdrawing money is encapsulated within the Banking Service. The User Management Service doesn’t know anything about this and, in many cases, the communication is implemented in an event-driven manner (by listening to interesting events), so they don’t even know the IP address of other services.
This independence is notable and very important as it really decouples the services.
As long as the services don’t break their public contract, changes made in the Banking Service will never impact or lead to changes in the User Management Service.
Now, what if we could adopt the Microservice approach for our in-memory application as well? What if we could use the same methodology also within our Microservices (or even Monoliths) ? What if we could introduce a completely new approach that can help us tackle the issues listed above related to the conservative Layers approach?
From Microservice to Microunit
Considering our Banking Service, we could adopt the Microunits approach, inside this microservice, and implement something similar to the diagram below.
The Banking Service is composed of three Microunits, each dedicated to a specific internal task: Validating the requested action, performing the action and finally notifying other services about the status of the action.
This introduces two layers of Separation of Concerns:
- Banking Service’s business logic is encapsulated inside dedicated “Microunits”
- Within each “Microunit” we still separate the various conservative general capabilities
Separating an implementation into Layers is not necessarily a bad thing. It has downsides, but these downsides are mostly due to the fact that the layers are implemented with only a single level of “Separation of Concerns” (where we handle many different business use cases scattered between those layers).
But with Microunit’s approach we can do better – split up our Microservice (or even Monolith) internal implementation into Microunits and encapsulate in each one of them their specific business logic. Now when touching the code within a Microunit we keep the others pretty safe thus increasing significantly the fragility of the entire service.
Although the implementation details can vary, at Imperva we implemented the Microunit approach in our Stepping framework.
A high level explanation can be found in this article, and a deeper documentation can be found in Stepping’s github repository.
At its core, Stepping is implemented by representing each Microunit as a Thread that listens to events sent to its private in-memory queue.
Once a new event arrives, the Thread retrieves the event and sends it to its Microunit which contains a specific Business Logic.
This way, the Cohesion of the Business logic is bound to very clear confines, and each Microunit is completely “unconscious” of other Microunits.
Furthermore, using separate threads to break the business logic into different units makes the separation more robust and harder to break by mistake.
This two-level Separation of Concerns and the event-driven pattern enable us to refactor a Microunit without fear of touching other units. We can even break the rigid Code Path, as we can now send and receive events in any possible combination.
In Stepping we introduced even more features from the Microservice world, including Microunits scale-up, distribution strategy, redundancy, and more.
Obviously the implementation is more complicated, but I hope the idea is clear.
The Microunits approach enables developers to create more granular separations between the different features, thereby decreasing the fear of breaking other business units-of-work.
This article should be considered a first thought of implementations when trying to adopt the Microservice concepts into our in-memory applications.
Within Imperva, many solutions are implemented with the Microunit pattern. Until now, developers have found it very useful, but we would like to hear your thoughts about it.
If you have any insights, questions or thoughts about this pattern, please feel free to reach us at email@example.com and start a discussion.