
Chunks of 'Clean Architecture' - Part 2/3
A series of posts on what makes a clean architecture according to Robert Martin. Part 2/3
Some of these notes are as they appear in the original book, most tailored by me, but all ideas are a creation of Martin or authors Martin quotes in his book.
This is Part 2. Check Part 1 here, and Part 3 here.
Part III: Design Principles
If the code is not clean, the architecture does not really matter. If the code is clean, the architecture can still be a mess. This is where SOLID principles come in.
SOLID principles tell us
- how to arrange functions and data into classes
- how to interconnect those classes
SOLID classes:
- tolerate change
- are easy to understand
- are the basis of other systems
SOLID principles summary:
- SRP: Single Responsibility Principle
- the best structure for a software system is heavily influenced by the social structure of the organization that uses it so that each software module has only one reason to change (a corollary of Conway's law)
- OCP: Open-Closed Principle
- systems that tolerate change are those that change by adding new code, rather than changing existing code
- LSP: Liskov Substitution Principle
- to build systems from interchangeable parts, those parts must adhere to a contract
- ISP: Interface Segregation Principle
- avoid depending on things that are not used
- DIP: Dependency Inversion Principle
- code that implements high-level policy should not depend on the code that implements low-level details. Details should depend on policies
C7: 🔂 Single Responsibility Principle
The first thing I think of when reading this name is that every module should do one thing. A function should only do one thing, but a module can do more than one thing. The distinction here is that a module should be responsible to one, and only one, actor (group of stakeholders that are the reason to change).
Separate the code that different actors depend on
If an Employee class can calculatePay() for a CFO, and can also reportHours to a COO, it's being responsible to different actors. Coupling can cause the actions of the CFO's team to affect something that the COO's team depends on, and possibly without noticing.
On top of that, multiple people changing the same source file for different reasons is a great recipe for problems and pain.
Perhaps the most obvious way to solve this is to use the Facade pattern.
EmployeeFacade class, which I would have called EmployeeService, just instantiates and delegates calls to the classes with the functions.
C8: 🌂 Open-Closed Principle
A software artifact should be open for extension but closed for modification.
Bertrand Meyer
In other words, the behaviour of an artifact ought to be extendible, without modifying the artifact.
We want to protect the Controller from changes in the Presenters. We want to protect the Presenters from changes in the Views. We want to protect the Interactor from changes in --well, anything.
Changes to the Database, or the Controller, or the Presenters, or the Views, will have no impact on the Interactor. Why? Because the Interactor contains the business rules, the highest-level policies that ought to be protected from changes made to lower-level components, the details.
If component A should be protected from changes in component B, then component B should depend on component A.
Directional Control
A very important point from the horrible diagram above is the fact that the FinancialDataGateway interface between FinancialReportGenerator and the FinancialDataMapper exists to invert the dependency that would otherwise have pointed the Interactor, the highest-level component, to the Database, in Martin's words, a detail. All arrows must "go in" the highest-level component, not out.
So the lesson here is to use a Gateway interface whenever we need to block transitive dependencies from details like controllers or databases, that would know too much about the entities/policies.
As much as we want to protect the Interactor from changes to the Controller, we also want to protect the Controller from changes to the Interactor by hiding the internals of the Interactor.
C9: 💱 Liskov Substitution Principle
This principle is basically the definition of a subtype: if a program P defined in terms of an object of type T is unchanged when T is substituted with an object of type S, then S is a subtype of T.
Let's go over this again by understanding what a violation of LSP looks like. In the example below, Square is not a proper subtype of Rectangle because the height and width of a rectangle are independently mutable; in contrast, the height and width of the Square change together. Since the User believes that r is a Rectangle, it could easily get confused if r ends up being a Square, as the failed assertion shows.
The only way the User has to defend here is to add some mechanism (an if clause) to detect whether r is a Square or not. And then he blew it: since the behaviour of the User depends on the types it uses, those types are not substitutable.
Users must depends on well-defined interfaces and on the substitutability of the implementations of those interfaces, not on extra mechanisms that pollute architectures.
C10: 👨🎨 Interface Segregation Principle
Code Violating ISP
User i only uses operation i from OPS. Any change in OPS will require all 3 User classes to be recompiled and redeployed (i.e. an effect of Java imports). User 1 is inadvertently depending on ops 2 and ops 3.
Code Compliant with ISP
A language issue
At the programming language level, only statically typed languages are vulnerable to ISP violations. Dynamic languages like Ruby or Python don't have to do recompilation and hence are not vulnerable to this.
Consequently, this is the primary reason why dynamic languages create systems that are more flexible and less tightly coupled than statically typed languages.
The lesson:
Depending on something that carries baggage that you don’t need can cause you troubles that you didn't expect.
C11: 🙃 Dependency Inversion Principle
The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions. That is, import statements refer only to interfaces or abstract classes, but nothing concrete.
Stable software architectures are those that avoid depending on volatile concretions, and that favour the use of stable abstract interfaces. One can add functionality to its implementations without making changes to the interface.
Coding Practices
- Don't refer to volatile concrete classes: use abstract factories
- Don't derive from volatile concrete classes: inherit responsibly
- Don't override concrete functions: abstract the function, and make implementations
The source code dependencies are inverted against the flow of control- which is why we refer to this principle as Dependency Inversion.
Part IV: Component Principles
Skipping this whole part - bunch of metrics nonsense
––––
Part V: Architecture
C15: 📐 What is Architecture?
Why should I care?
There are many systems out there with terrible architectures that work just fine. Their troubles do not lie in their operation or in supporting the uses cases, but in their deployment, maintenance and ongoing development.
What is a good architecture?
A good architecture maximizes programmer productivity and minimizes the lifetime cos of the system.
Common pitfalls
- Architectures that make a system easy to develop, but difficult to deploy.
- Small teams may be faster initially without an architecture, but as the systems grows out into multiple teams and components, progress cannot be made unless an architecture helps in dividing the system into reliably stable interfaces.
- Hardware is cheap, people are expensive: inefficient architectures can often be made to work effectively simply by adding more storage and servers, but will also start hiding operational needs.
- An architecture poorly thought-through will increase spelunking (cost of digging through existing software) and risk.
Keeping options open: Policy
All software systems can be decomposed into policy and details. The policy embodies all the business rules; it’s the true value of a system.
The way you keep software soft is to leave as many options open as possible, for as long as possible. The longer you wait, the more information you have with which to make proper decisions.
A good architecture maximizes the number of decisions not made.
Which options?
These options are the details: anything necessary to communicate with the policy, but that do not impact the behavior of the policy. For example, IO devices, databases, web systems, servers, frameworks, communication protocols, etc.High-level policy does not care which kind of database will be used, whether information will be delivered over the web or not, which interface is used to the outside world, which dependency framework is adopted, etc.
So for example we can have a UserRepository interface implemented by a concrete Postgres client for pre-prod and prod environments, and an in-memory fake implementation for tests stages.
C16: 🦋 Independence
Operational
An architecture that maintains the proper isolation of its components, and does not assume the means of communication between those components, will be much easier to transition through the spectrum of threads, processes and services as the operational needs of the system change over time.
Development
If a system is properly partitioned into well-isolated, independently developable components, Conway’s Law comes into play:
Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.
Deployment
A good architecture helps the system to be immediately deployable after build. This can be achieved through “master components” that tie the whole system together and ensure that each component is properly started, integrated and supervised.
This is the case at Amazon, where code pipelines would have component packages auto-build into it, so development stays independent, and releases for those components are packaged together into version sets that are then tested throughly throughout different sequential stages in a code pipeline. MercadoLibre had a similar situation for its iOS and Android mono-repos.
Welcome to the real world
The goals engineers must meet are indistinct and inconstant. If we partition our systems into well-isolated components that allow us to leave as many options as possible, for as long as possible.
Decoupling layers
A good architecture would want to separate the UI portions of a use case from the business rule portion in such a way that they can be changed independently of each other. For example, the validation of input fields is a business rule that is closely tied to the application itself. In contrast, the calculation of interest on an account is a business rule more closely associated with the domain.
Different kind of rules will change at different rates, for different reasons.
Decoupling use cases
Use cases are narrow vertical slices that cut through the horizontal layers of the system. If you decouple the elements of a system, you can add new use cases without interfering with old ones.
Decoupling mode
Splitting by use cases also helps with operations. High throughput use cases are likely separated from those that run at a low throughput. They must be independent services that know how to communicate over a network of some kind.
Independent Developability
As long as the layers and the uses cases are decoupled, the architecture of the system will support the organization of the teams.
Independent Deployability
If the decoupling is done will, the it should be possible to hot-swap layers and use cases in running systems, i.e. adding a few new jar files.
Duplication
When vertically separating use cases, you may be tempted to couple the use cases because they have similar algorithms or schemas. Be careful: make sure the duplication is real, that is, that both sections of code evolve at the same time.
When horizontally separating layers, you may be tempted to couple similar data structures for the database and for the screen view. Be careful: a separate view model is not a lot of effort, and it will help you keep the layers properly decoupled.
Martin’s preference: push the decoupling to the point where a service could be formed, should it become necessary, but then leave the components in the same address space as long as possible. This keeps your options open.
A good architecture will allow a system to be born as a monolith, deployed in a single file, but then grow into a set of independently deployable units, and then all the way to independent services and/or micro-services. Later, as things change, it should allow for reversing that progression and sliding all the way back down into a monolith.
Notably, a Amazon Prime Video video quality analysis service slid back to a monolith from Step Functions, saving serialization and networks back-and-forths, and 90% in infra costs (my team was tangential to this, sadly).
C17: 〰️ Boundaries, Drawing Lines
Sometimes we developers rush to prematurely adopt and enforce a full-blown service-oriented "architecture", with a massive suite of domain object services, only to find out later that whatever was built will be deployed in a single server.
This of course comes with a huge cost in person-hours. That is why decisions about frameworks, databases, web servers, dependency injection and the like should be deferrable. A good architecture does not depend on these decisions.
For example, one could delay the decision of choosing MySQL as a database by putting an interface between all data accesses and the data repository itself. So by placing the database behind an interface, the business rules only need to know that there is a set of functions to fetch and to save data.
Figure 17.2 from Clean Architecture
The direction of the arrow crossing the boundary is important. It shows that the Database component does not matter to the BusinessRules component (or that the BusinessRules are independent from the Database), but the Database cannot exist without the BusinessRules.
This boundary also exists between BusinessRules and GUI components, since they change at different times and at different rates. GUIs and Databases are plugins that are not related to the core business. Their dependency arrows should point to the core business (Dependency Inversion Principle, Stable Abstractions Principle: dependency arrows are arranged to point from lower-level details to higher-level abstractions).
C18: 🧱 Boundary Anatomy
When one source code module changes, other source code modules may have to be changed or recompiled, and then redeployed. Managing and building firewalls against this change is what boundaries are all about.
When a high-level client needs to invoke a lower-level service, dynamic polymorphism is used to invert the dependency against the flow of control. Higher-level components remain independent of lower-level details (ServiceImpl), making the latter as plug-ins of the former.

C19: 🎚️ Policy and Level
A computer program is just a detailed description of the policy by which inputs are transformed into outputs.
Policies that change for the same reason, and at the same time, are at the same level and belong together in the same component.
The art of architecture often involves forming the regrouped components into a directed acyclic graph.
Low-level components (details) are designed so that they depend on (import) high-level components (abstractions).
It is far more likely that IO devices will change than that the encryption algorithm will change.
C20: 💼 Business Rules
Critical Business Rules: rules that make or save the business money, either implemented on a computer or manually.
Use case: a use case describes application-specific business rules as opposed to the CBR within the entities. It specifies the input, output and processing steps involved. They contain the rules that specify how and when the CBR within the Entities are invoked. They control the dance of the Entities. They are objects: it has one or more functions that implement the application-specific business rules. They are low-level (closer to inputs, outputs).
Entities have no knowledge of the use cases that control them (Dependency Inversion Principle). They are high level (farther from inputs, outputs).
C21: 😱 Screaming Architecture
A good architect ensures that the homeowner can make decisions about the exterior material (bricks, stone, etc.) later, after the plans ensure the use cases are met.
Frameworks are tools to be used, not architectures to be conformed to. If your architecture is based on frameworks, then it cannot be based on your use cases. They are options to be left open.
A good architecture makes it unnecessary to decide on Spring, Hibernate, Tomcat until much later in the project. Indeed, the decision that your application will be delivered over the web is one that you should defer. You should be able to deliver it as a console app, or a web app, or a thick client app, or a web service app.
You should not need the database connected to run your tests. Your Entity objects should be plain old objects that have no dependencies on frameworks or databases or other complications.
New programmers should look at the source repository and it should scream what is is: a health care system, a note taking app, etc.
Comments
Thank you for your comment! Under review for moderation.