I’ve been building and maintaining software for a while, and the rules of design are visceral: I feel that something is sensible, or problematic. As I spend more of my time mentoring & leading others though, it becomes less important to know what is wrong or right, and more important to be able to communicate ideas about good design. Because my decisions had become instinctive, embedded, I’d lost some of the ability to explain what I meant and why. Certainly, the principles ingrained in me had long since lost their English translations.
To solve this problem, I revisited a (not-so-old) classic, Clean Architecture by Bob Martin. Bob’s discussion of layered design in that book is fantastic. Clear, yet broad and virtually universal, it is still the mental model I use.
In an effort to cement my ability to communicate these concepts to colleagues and friends, I wanted to get some reps in here.
What is layering?
Layering is an arrangement of dependencies hierarchically, stacked or encapsulating one another. Each layer is distinctly separate, rather than intermingled. Each layer can only communicate with the layer above or below it. This creates increasing levels of abstraction.
We generally divide layers based on the level of generality vs specificity of the work each does. This will become more clear as we get to concrete explanations below.
Firstly, the formal separation which layers enforce makes maintenance much easier. We can manage stability per layer, and clearly define the dependency graph. Each layer does a certain job, extending the single responsibility principle.
Layering makes the connections between levels of specificity explicit. We can manage this coupling with interfaces, producing a layer-to-layer abstraction, to allow easy extension and substitution. The result is a system of self-sufficient layers, that can do their part, and then hand off to the next layer.
We see layered design everywhere in nature. The human digestive system is layered, first at the organ level, where digested organic material passes through the stomach, and intestinal layers, and also at the symbiotic level within each organ. The muscular-skeletal system is another example, and on a macro scale, whole ecosystems are layered, with different organisms contributing at different levels.
Human design uses layering ubiquitously. Every organisation is layered, from front-line workers up through the hierarchy, getting more and more general at each rung. Services are layered. In a restaurant, your food order is processed in a layered way, from the waiting staff, through the kitchen. The postal service is another even more extensive example.
That’s not to mention computing systems which are all layered. The OSI & TCP/IP models are famous examples, as well as the layers that make your computer, from the electronic components, up to the logic gates and latches, to micro-chips and embedded logic, interface buses and controllers, Kernels and Operating Systems, and eventually the user-space applications that we interact with.
In every case, each layer has knows only how to use the layer below, and what to do with it.
Layering our applications
We should layer our applications too.
Clean Architecture provides a framework for thinking about application layering, visualised as a set of concentric circles. The concentric circles make clear the direction of dependency & stability. I’ll step through these layers from the inside out.
“Entities” i.e. Business Rules
The inner-most layer comprises the business rules, the domain logic, or “entities” in OOP. These are the rules that implement the logic of the actual business, as it exists in the real world. For a bank, this would be something like applying interest to a loan, and for a delivery service, it might be that stores that are closed can’t accept orders.
When starting a new project, the business rules should be where you start. They are the most stable part of your system, and only change when the business changes. Everything else is volatile, determined by tech or product decisions, but the business rules rarely change.
“Use Cases” i.e. Application Rules
The next layer, wrapping the business rules, is the application rules.
This layer makes use of the business rules to create functionality for your applications. The bank will have a flow for granting loans, which will use the loan business rules already mentioned. Delivery applications will make use of the store rules to determine which stores should be displayed.
The adapter layer comes next. It is a translation layer, that sits between the application rules and the infrastructure. This is an important layer, because the infra will chop and change, as will the data structures it uses. We can’t rely on these data structures in the central layers because that would create too much volatility, so the adapters translate those structures to match the signatures of the application layer. Therefore, if the infra changes, the data model changes, or the body of a request changes, we modify the adapters and leave the core layers unchanged.
Service controllers often act as adapters. They keep the protocol & the data model separate from the logic.
On the outside we have the infrastructure.
This refers to the compute resources we think of as infrastructure, but also anything else that is implementation detail to the system. This includes http and pub-sub handlers, which are specific to the chosen protocols. At any time we could swap one for another. Infra includes the UI, which is likely to change often and require new and different functionality from the server. This layer of course includes the database, and its data model too. All these things live outside of an adapter layer.
This layer is the most volatile. It can and does change often. It is also the most specific. The choice of request body structure, or message broker, must be kept at arm’s length from the core business logic which is not going to change too much at all. Doing this preserves the stability of these inner layers.
Key Principles for Concentric Layering
There are certainly others, but these are most important and most often abused in my view.
1) Layers should only depend inward
This is key and inherent to concentric design. On the inside, the business rules depend on nothing. They are self-contained and self-sufficient. In this way, they remain absolutely stable, unaffected by changes to less stable packages.
If you find your business logic depending on outer layers, perhaps as an input structure for example, use dependency inversion, through injection or some factory, to remove that dependency.
This is a very strict rule. Any outward dependence destroys the stability tree.
Most importantly, business logic should know NOTHING of any other layer.
2) Business objects must be separate from the data model
This is one of the most common mistakes I see. Neatly defined layers, but the database model maps directly onto the business objects. This violates rule (1) above.
With such a mapping, even if your business logic doesn’t have a direct or explicit dependency on the data model, you’ve created at least a cognitive dependency. Someone working on the business logic will need to know about the data model, and vice versa. Changing one suggests you need to change the other to match, even if you strictly don’t. It creates an expectation of dependency. If you add a new field to the data model and not to the business logic, now you’ve got confusion because thus far you’ve assumed the two are the same.
Avoid this problem by making them clearly separate from the outset.
That’s a quick overview of layered design as professed by Clean Architecture. I really do recommend reading the book, which has much to offer beyond the points I’ve made here.
Layering is the easiest method I know for keeping things clear, for managing stability and maintainability. Whether the layer is an entire service, a module or even a single function, splitting your software this way ought to be the default.
Happy learning !