Rober Martin, in his book – Clean Architecture, has cleared many misconceptions when it comes to architecting the system. Here, we will cover some of the most important points that are covered by the book and look at how we can implement them in our day to day work.

Clean Architecture can be studied much easily with the basic knowledge of OOP and SOLID principles. We shall not be covering those here. However, we will talk about SOLID principles in general. So what are these principles:

Single Responsibility Principle: There can be only actor who/which can introduce the change in the module.

Open Closed Principle: Module is open for extension via addition but closed for changes in the existing code base

Liskov Substitution Principle: A module that depends on another module can have the dependency module replaced with another.

Interface Segregation Principle: Programming to interfaces

Dependency inversion principle: Instead of having the control flow from top to bottom, use interfaces to remove those dependencies and invert the dependency from bottom up, where bottom (minuscule modules/functions) can be substituted.

Architecture

Architecture of the system is more related to the development, maintenance and deployability of the system than anything else. Robert Martin

Hexagonal Architecture Conceptulisation

Architecture of the system tends to impact development, deployment, operation and maintance of the system. Therefore, it is the primary responsibility of the architect to ensure that the system architecture address these specific concerns in the system.

Development:

Team management inline with component management

How developers can get things done without being blocked by other teams or developers

How the teams interact with one another

Deployment

Deployment should happen as a single action

What kind of components structuring suites the team and the deployment process

Depoloyment should happen more often and independent of the other teams or components

Operation

Architecture impacts least on the operation factors. Although, the architect is responsible for the needs of high performance or quality assessment of the value the system generates. But it tends to be lower on the scale than other aspects listed here.

Cost equation shows that the cost of maintainence and development issues are far more than the performance issues and as we all know: Premature optimization IS the root of all evil.

Maintenance

Digging existing system is hard and changing it is harder. Therefore, the principles of architecture must take into the account the maintainabilty of the system.

Risk of breaking what already works is very high and hence, the system architecture must be designed with change in mind.

Screaming Architecture

The system code base should scream the to the reader what the system does as opposed to what the system is built using.

Often, most code bases when viewed by a new developer on the team tells him/her that the system is a web application and what framework was used to build it. It beats the purpose, because every system is built for a purpose and that purpose must be made explicit with the way the code is structured. As we will see, most of the details that code reader first see such as whether it web application or what database it uses is just details. We shall see what we do with details soon.

Business Rules and Policy

Every system is built for a purpose as we discussed earlier. This purpose usually is associated with the business that the system purports to solve the problems for. These business domains have rules and policies, many of which are independent of the system itself. For example, in stock markets; an exchange will always be an exchange and perform the duties of the market regardless of which system you model it in. These business rules are also called Application Independent Rules, which suggests the fact that the system itsle is built to model these rules. However, there are other set of rules that are specific to application. For example, a banking application will have account transfer functionality regardless of which bank is building the system; but the commission rules per transaction will change from one banks system to another based on how the banks allow their users to interact with the system. May be, a specific set of customers will not be allowed to make any online transactions for a set period, while their documentation is being processed and so on. These rules are specific to application and are either known as features or use cases. They suggest how the system will be interacted with by the indented users of the system. These rules are called Application Specific Rules.

The role of architect in this segregation is to make Application Independent Rules as the core model of the system, which will encapsulate those rules and will be more abstract and stable components. While all the Application Specific Rules will tend to be more unstable and change with the requirements of the user of the system, which is most of the time. Hence, it is imperitive that the Application Specific Rule encapsulating components should depend on Application Independent Rules. A good way to envision this is models in the system would represent the AIR and services will represent the ASR.

Services and Boundaries

Boundaries ensure the separation of concerns with the flow of dependencies going from volatile & concrete modules to the stable and more abstract components.

Boundaries in the system can manifest on many levels and in different types of deployment mechanisms. As can example consider the following:

Monoliths: In case of monoliths the deployment happens for the entire software at the same time. Therefore, the components tend to have loose boundaries, but the place where it can be applied is at the source code level. This helps with multiple teams working on the same system to decouple their development efforts from that of other teams working on a separate component within the system. Multiple Systems Deployment: Besides having source code level boundaries, there can be further division in the system based on the deployment artifacts such as jar, gems, pip wheels, etc. Their independence to deploy per team is afforded with the help of such a boundary. Service Level Boundaries: These are the most granular level of putting boundaries. Each service will have its own code repository, deployment pipeline and team, therefore dividing the system on execution unit level.

Partial Boundaries are useful in the case where we are starting out with a project and are currently unsure about the number of services the system will requires. Also, the initial team will also be small to divide components per team. Hence, it is feasible to have a monolith application being worked on by the same team but boundaries can be maintained with the use of ISP principle to segregate components as per service. Later these services can be separated completed from one another by implementing the appropriate proxy as the service implementation which can call the remote service after it has been divided away.

Tests are part of architecture and must be viewed as a separate component with independent deployability, where nothing depends on tests while tests depends on everything else.

Care should be taken such that the components are designed with the testability in mind.

Remember YAGNI (You ain’t gonna need it). Do not over engineer till you need it to be so.

Again, the cost of adding boundaries later is much higher than spending some time building partial boundaries.

Boundaries are used to isolate the service in such a way that there is loose coupling between the user of the service and the component providing the service itself. However, there are a lot of cases where multiple services have common data dependencies and their separation is just a mirage. It is better to identify such dependencies when demarking the component boundaries. Just like in OOP methodology, you encapsulate the data, similarly; the services too should encapsulate all its data requirement within its own component.

Things to remember:

Even SOA and microservices do not gurantee proper boundaries due to common data dependencies If services are split by functions alone, then the change would affect multiple services. SRP principle. Services can further have boundaries in the form of Layers. Here, the kind of work that happens within the service is divided into layers. For example, data access layer, business logic layer and so on.

System Details

It is the job of the architect to defer decisions as much as possible.

A small detail about system details. All the technological stack aspect of the system usually tend to be details. Web framework, Database, IO, etc are all details and the decision to use a specific database, web framework, etc should be deferred as long as possible. Imagine the situation where if we use a specific database from get go then all the code base will have some footprint of the database usage and if later, due to scaling issues, we decided to change the database, then all these footprints will be difficult to dig and replace. Even the way system behaves changes with the database used. Now let’s consider the possibility of mocking up some repositories that fake the data as coming from database, then we can build all the stable core of the system without having to even require a database for a very long time. Same with web framework. If we simply take request parameter in some kind of abstract object and return response again in somekind of DTO, then we do not have to worry about which framework to use as it can be plugged in later to use these request and response DTOs.

Component Based Architecture

How do we define a component? A component is a module/class or any unit that forms a cohesive whole (a complete subset) of any given system. The component must follow the SRP and OCP principles at the least, so be called as such. Clean Architecture can also be expressed as Comp-nent based architecture. This brings us to the next level of principles that are important when considering COA.

Cohesion Principle

Reuse / Release Equivalence Principle (REP): This basically means that the component should be cohesive so as to be changed for a single reason (SRP), which allows is to be released and reused as a single unit.

For example: A json library within the code base that is used for converting dictionary or other such data structure to string and vice versa. This can be safely assumed as a single component which will be changed as a result of specific reason.

Common Closure Priciple (CCP): This is another point in favor of making classes cohesive that change together as a unit. It too reinforces the idea of SRP and OCP. Continuing with the above example, all code / classes related to json library will be within a single component and will expose and interface with which it would be accessed.

Common Reuse Principle (CRP): This is opposite idea to the above two. REP and CCP tries to assimilate similar things into a single component, whereas, CRP divides the component to as much granularity as acceptable. It requires that the simple component should not contain anything that might change or multiple reasons and move those classes/modules to different component.

REP, CCP and CRP Tension

In the above figure, REP groups for resuers, CCP groups for maintainers and CRP splits to avoid unneeded releases.

Component Coupling

Acyclic Dependency Principle: This is simple to explain. There must be no cycles of dependencies. For example if A depends on B and B depends on C, then C must never be dependent on A or B. It is not easy to forsee the cycles forming between the components in the system and whenever it is identified then programming to interface and dependency injection can be leveraged to avoid such cycles.

Stable Dependency Principle: It basically states that the dependencies should be more towards the components that are bound to be stable and away from components that are unstable. Which means, if the component A is not going to change over a long period of time time in the system and B will change as per user requirement changes per sprint iteration, then it is necessary to only allow B to depend on A and never the other way round. The reason is simple, if A depends on B and if B changes as requirements change then A will also have to be recompiled alongside B or at least throughly tested whenever B changes. Leverage dependency inject to invert the dependency and interface segregation principle to keep A independent of changes in B

Stable Abstraction Principle: The degree of abstraction should increase with the stability of the component. Concrete (throw away or specific) components tend to be unstable and hence they should be hidden beneath the abstractions of interfaces and use dependency injection to avoid any other component from directly dependening on these unstable components.