Implementing Clean Architecture in Python | Andros Fenollosa
Skip to content
Clean Architecture is a variant of Alistair Cockburn's hexagonal architecture. The main idea is to separate the business logic from the infrastructure. Proposed by Robert C. Martin in 2012, it combines the principles of hexagonal architecture, onion architecture and other variants, defining more precisely the responsibility of each layer and how they should communicate with each other.
A project is divided into different layers:
🟡 Entities : Business variables/constants/classes/objects. The core of the application. For example, User, Product, Invoice, etc.
🔴 Use Cases : The implementation of the business logic. Mainly functions. For example, the logic to create a new user, calculate the total of an invoice, etc.
🟢 Gateways : The interfaces that the Use Cases need to interact with the outside world. For example, the database interface, the file system interface, etc.
🔵 External Interfaces : The applications that interact with the outside world.<br>Database
Frameworks
ORM
File system
Devices
External APIs
UI<br>HTTP (website)
CLI (command line interface)
API (REST API)
The main general rule is that inner layers cannot depend on outer ones: dependencies always point inwards. When an inner layer needs something from the outside world (a database, an external API), it does not import it directly: it receives an interface provided by the outer layer, and it only returns simple structures (in Python we will use dictionaries).
⬆️ Calls come in through interfaces | ⬇️ Results go out as simple structures
And we will never break these rules. Even when an exception occurs, we will handle it and return a structure.
Advantages
Let's go over some of the advantages why we should implement clean architecture in our projects.
Maintenance and modularity : The separations make it easy to make changes without affecting other parts of the code.
Easier testing : Since all components are independent, we can test them in isolation. We know what we receive and what we must return, regardless of the interface or technology behind it.
Code reuse : We can reuse use cases in different interfaces, or entities in different use cases.
Technology isolation : We can change elements such as frameworks, databases, UIs, etc. without affecting the business logic.
Scalability : We can add new features without affecting the existing ones.
Documentation : Having a well-defined structure, the documentation will be easier to write.
All of this results in more robust, cleaner and easier to maintain code.
Disadvantages
Not everything is rosy. Clean architecture also has its drawbacks:
Increases initial development time : Having to define the structure and the interfaces, the initial development time can be longer. Thorough upfront planning is required.
Learning curve : At first it can be difficult to understand how the different layers communicate. The team must be trained and know the architecture.
Abstraction overhead : Creating many interfaces and layers can result in abstraction overhead. Be careful, it is not necessary to create an interface for every function, only for those that interact with the outside world.
File fragmentation : Having different layers, each in its own folder, can result in a considerable file tree.
However, with practice and training, these disadvantages can be mitigated.
Simple implementation example in Python
Let's look at an example of a feature that calculates the installation price of a turbine. We will need, from the user, the number of turbines to install. The price for each turbine installation will be a constant.
The folder structure will be as follows.
mi_proyecto : The main folder. The project name.<br>core : Business logic.<br>entities<br>constants.py
use_cases<br>turbine<br>calculate_turbine_installation.py
infra : Infrastructure or external interfaces.<br>cli<br>click<br>src
api<br>flask<br>src
fastapi<br>src
http<br>django
As we said before, the installation price will be a constant. We will create a constants.py file in the entities folder.
# mi_proyecto/core/entities/constants.py<br>INSTALLATION_PRICE = 1250<br>Business classes and objects also live in entities. In this article the constant is enough, but if the project grew, the turbine would have its own entity.
# mi_proyecto/core/entities/turbine.py<br>from dataclasses import dataclass
@dataclass<br>class Turbine:<br>model: str<br>power_kw: float<br>The use case will be in the use_cases folder.
# mi_proyecto/core/use_cases/turbine/calculate_turbine_installation.py<br>from mi_proyecto.core.entities.constants import INSTALLATION_PRICE
def calculate_turbine_cost_use_case(number_of_turbines: int) -> dict:<br>total = number_of_turbines * INSTALLATION_PRICE<br>return {"total": total}<br>The first external interface will be a REST API. In the example we will use Flask for simplicity.
We can have different implementations in different frameworks. Therefore, each repository (not to be confused with a...