By Hassib Moddasser
Published on Dec 12, 2021 · 12 min read
You may have previously heard of the term Clean Architecture quite a lot, especially if you are in a development team, or maybe surfing the web, reading articles, or even getting recommended on YouTube to watch some videos. Still, you did not exactly grasp the concept. Do not worry; you are not alone; it was unclear to me what Clean Architecture was and how to use and leverage it.
The secret to building a large project that is easy to maintain and perform better is to separate files and classes into components that can change independently without affecting other components: this is what Clean Architecture is.
Clean Architecture is an architectural style created by Robert C. Martin. It is a set of standards that aims to develop an application that makes it easier to quality code that will perform better, is easy to maintain, and has fewer dependencies as the project grows.
This article will be different from any other article you can find on the web because I've included a real-life scenario. Firstly, I will introduce what Clean Architecture is. We will then head to the development side of things to see how the engineering team at Merlino Software Agency developed the OneFood application using ExpressJS with a Clean Architecture. In the end, we will conclude how Clean Architecture makes a difference. OneFood is a premium food delivery Startup currently operating in Italy, Germany, and Austria.
Let us have a quick introduction to Software Architecture before diving into it!
What is Software Architecture
Before providing a definition, consider that the actions people may do using systems on a daily basis — scheduling a Tweet to post, sending an automated transactional email, etc. — heavily depend on the Software Architecture of the system in use.
Many things we know and use would be impossible without software architecture, but why is that?
A system's architecture acts as a blueprint. It establishes a communication and coordination mechanism among components, gives an abstraction to control system complexity, and represents the design decisions related to overall system structure and behavior.
In simple words, Software Architecture exposes a system's structure while hiding implementation details. Moreover, it also focuses on how the elements and components within a system interact with one another.
The subject of Software Architecture appears to have sparked much attention in recent years. Furthermore, among the many different types and styles, the Clean Architecture attracted more attention than the others.
Let us head to the central part of the article and find out what Clean Architecture is.
The Clean Architecture
Microservices and serverless architecture have emerged as revolutionary architectural patterns in the software development world during the last several years. Each has its characteristics, behaviors, technological structure, and required expertise. However, they are all part of a broader desire for a better separation of architecture concerns and improved testability.
Clean Architecture is a set of standards that aims to develop layered architectures that make it easier to write quality code that will perform better, is easy to maintain, and has fewer dependencies as the project grows. This architecture can be applied no matter what language you code in.
The illustration below shows the four concentric circles that compose a Clean Architecture, how they interact with each other, and their dependencies.
The Dependency Rule
In the illustration above, please pay attention to the arrows, the fact that they are pointing from the outermost circle down into the innermost circle, and that they only go in one direction. The arrows represent the dependence flow, indicating that an outer ring can depend on an inner ring but that an inner ring cannot rely on an outer ring; this is what The Dependency Rule is all about.
For example, Entities know nothing about all the other circles, while the Frameworks & Drivers, the outer layer, know everything about the inner rings.
In simple words, variables, functions, classes, or any other named software entity declared in an outer circle must not be mentioned by the code in an inner circle.
Four layers of Clean Architecture
There are four concentric circles in Clean Architecture that each represents different areas of Software, which are as below:
- Use Cases
- Interface Adapters
- Frameworks and Drivers
The development team of Merlino also implemented another layer called Routes, and that's totally okay. If you find that you may need more than four, just go on. However, the Dependency Rule must always apply.
So, the four layers becomes five layers in our case study, which are as below:
- Use Cases
- Interface Adapters
- Frameworks and Drivers
Let us find out what each circle means along with our case study, but before diving in, here is the project’s hierarchy;
plaintext.│ README.md│ package.json│ ...│└─── app│ │ app.js│ │ config.js│ │ database.js│ │ logger.js│ │ server.js│ ││ └─── controllers│ │ │ order.controller.js│ │ │ ...│ ││ └─── data-access│ │ │ index.js│ │ │ order.db.js│ │ │ ...│ ││ └─── entities│ │ │ order.entity.js│ │ │ ...│ ││ └─── routes│ │ │ order.routes.js│ │ │ ...│ ││ └─── use-cases│ │ └─── orders│ │ │ │ index.js│ │ │ │ post-order.js│ │ │ │ ...│ │ └─── ...│└─── ...
At the center of the onion are the Entities of the software, which constitutes the business logic of software. An entity can be an object with methods, or it can be a set of data structures and functions, they don't know anything about the outer layers and they don't have any dependency. They encapsulate the most general and high-level rules that the application would use.
In simple words, Entities are the primary concepts of your business.
When something external happens, Entities are the least likely to change. A change to page navigation or security, for example, would not be expected to affect these objects. The entity circle should not be affected by any operational changes to any application.
If you’re building a Social Media application, you might have entities like Posts, Comment, or User, or in the case of OneFood — a premium food delivery business and a client of ours — a few of their entities would be Customer, Order, Payment, Product, Vendor, etc.
Let us jump to the code and look at what an Order entity would look like in the OneFood application, declared as a Mongoose schema:
All Entities can then be exported like this:
But how can we use other libraries or tools within our Entity?
The answer is Dependency Injection. Somewhere upstream, we have some code that will call the buildMakeOrder function and inject all the various dependencies required.
Dependency Injection has several advantages described below:
- The first advantage is Testability. By injecting dependencies, we can test our code independently of anything else.
- The other advantage is as the developer is writing this code, they do not need all of these dependencies to be ready. In this case, the developer can focus on the business logic of the Entity.
- The most important advantage of doing this is that the developer can change the implementation details of the project dependencies independently of this code.
2. Use Cases
The Use Cases layer, which lies outside the Entities layer, contains login and rules related to the behavior and design of the system.
In simple words, Use Cases are interactions between Entities. For example, suppose we are in our Social Media application example. In that case, we can have a Use Case like user posts, or in the OneFood application, a customer places an order.
Changes to this layer should not affect the entities. Changes to externalities such as the database, user interface, or frameworks are unlikely to affect this layer.
Let us check out the /use-cases directory in the OneFood application and look at the Order’s entity interactions:
All Use Cases can then be exported like this:
3. Interface Adapters
The Interface Adapters or the Adapter layer holds the controllers, APIs, and gateways. The Interface Adapters govern the flow of communication between external components and the system's back-end.
In simple words, Interface Adapters are isolating our various Use Cases from the tools that we use.
Let us look at the OneFood application’s controllers directory and choose OrderController by sample to see what’s going on:
The /routes folder is where you can organize all of your different REST endpoints declarations. The file below exposes all available endpoints related to the Orders Entity:
Bonus: you may have noticed the makeExpressCallback function in the code. That is a wrapper function that:
`as inputs from Express's
`functions defining each endpoint.
- creates a clean request object with all possible useful information needed by controllers and feed them with it (i.e.,
- handles whatever it's returned by controllers (i.e., data or errors), and replied to clients in a standardized way.
Check this file out:
Then, let's export all routes:
5. Frameworks and Drivers
The Frameworks and Drivers, also known as the Infrastructure Layer, is the outermost layer that provides all necessary details about frameworks, drivers, and tools such as Databases that we use to build our application. All the details of the system go in this layer.
For example, in the case of the OneFood application, the engineering team used Express JS as the framework and MongoDB driver for Node JS as a database driver.
The entry point to the entire application is the app.js file. This is where we use the Express JS framework. You will see many import statements, initialization of Express server, using the routes middleware, and finally exporting the app module.
Following these guidelines is simple and will save you from a lot of trouble in the future. By separating Software into layers and obeying the Dependency Rule, you will create a Software that is more:
- Independent of a framework
- Independent of a database
- Independent of UI
- ... and independent of any other tools and drivers
Keep in mind that there’s no boundary of having just four circles/layers. If you find that you may need more than four, just go on. However, the Dependency Rule must always apply.