π This FastAPI-based project and its documentation represent my interpretation of Clean Architecture principles with subtle notes of Domain-Driven Design (DDD). While not claiming originality or strict adherence to every aspect of these methodologies, the project demonstrates how their key ideas can be effectively implemented in Python. If they're new to you, refer to the Useful Resources section.
π¬ Feel free to open issues, ask questions, or submit pull requests.
β If you find this project useful, please give it a star or share it!
This repository may be helpful for those seeking a backend implementation in Python that is both framework-agnostic and storage-agnostic (unlike Django). Such flexibility can be achieved by using a web framework that doesn't impose strict software design (like FastAPI) and applying a layered architecture patterned after the one proposed by Robert Martin, which we'll explore further.
The original explanation of the Clean Architecture concepts can be found here. If you're still wondering why Clean Architecture matters, read the article β it only takes about 5 minutes. In essence, itβs about making your application independent of external systems and highly testable.
Figure 1: Robert Martin's Clean Architecture Diagram
"A computer program is a detailed description of the policy by which inputs are transformed into outputs."
β Robert Martin
The concentric circles represent boundaries separating layers, each with its own policies. The less likely a policy is to change, and the more abstract and independent of implementation it is, the closer it is to the center. An example of the least abstract policy is an I/O operation.
The meaning of the arrows in the diagram will be discussed later. For now, we will focus on the purpose of the layers.
- The core of the application, containing entities, value objects, and domain services that encapsulate critical business rules β fundamental principles or constraints that define how the business operates and delivers value. In some cases, these rules can be seen as mechanisms that create the product's value independently of its software implementation. Changing them often means changing the business itself.
- It establishes a ubiquitous language β a consistent terminology shared across the application and domain. This is the language you can speak with managers.
- It's the most stable and independent part of the application.
Note
The Domain layer may also include aggregates (groups of entities that must change together as a single unit, defining the boundaries of transactional consistency) and repository interfaces (abstractions for manipulating aggregates). While these concepts aren't implemented in the project's codebase, understanding them can deepen your knowledge of DDD.
- This layer contains applied business logic, defining use cases β high-level abstractions that bridge the domain layer with its practical implementation, orchestrating business logic to achieve specific goals.
- Its core component is the interactor, representing an individual step within a use case.
- To access external systems, interactors use interfaces (ports), which abstract infrastructure details.
- Interactors can be grouped into an application service, combining actions sharing a close context.
Note
Domain and Application layers may import libraries that extend the language's capabilities or provide general-purpose utilities (e.g., for numerical computations, timezone management, or object modeling). However, they should avoid any ties to specific frameworks, databases, or external systems.
- This layer is responsible for adapting the application to external systems.
- It provides implementations (adapters) for the interfaces (ports) defined in the Application layer, allowing the application to interact with external systems like databases, APIs, and file systems while keeping the business logic decoupled from them.
- Related adapter logic can also be grouped into an infrastructure service.
Important
- Clean Architecture doesn't prescribe any particular number of layers. The key is to follow the Dependency Rule, which is explained in the next section.
A dependency occurs when one software component relies on another to operate. If you were to split all blocks of code into separate modules, dependencies would manifest as imports between those modules. Typically, dependencies are graphically depicted in UML style in such a way that
Important
A -> B
(A points to B) means A depends on B.
The key principle of Clean Architecture is the Dependency Rule. This rule states that more abstract software components must not depend on more concrete ones. In other words, dependencies must never point outwards within the application's boundaries.
Important
-
Components within the same layer can depend on each other. For example, components in the Infrastructure layer can interact with one another without crossing into other layers.
-
Components in any outer layer can depend on components in any inner layer, not necessarily the one closest to them. For example, components in the Presentation layer can directly depend on the Domain layer, bypassing the Application and Infrastructure layers.
-
However, avoid letting business logic leak into peripheral details, such as raising business-specific exceptions in the Infrastructure layer or declaring domain rules outside the Domain layer.
-
In specific cases where database constraints enforce business rules, the Infrastructure layer may raise domain-specific exceptions, such as
UsernameAlreadyExists
for aUNIQUE CONSTRAINT
violation. Handling these exceptions in the Application layer ensures that any business logic implemented in adapters remains under control. -
Beware of introducing elements in an inner layer tailored specifically to the needs of an outer layer. For example, you might be tempted to place something in the Application layer that exists solely to support a specific piece of infrastructure. At first glance, based on imports, it might seem that the Dependency Rule isn't violated. However, in reality, you've broken the core idea of the rule by embedding infrastructure concerns (more concrete) into the business logic (more abstract).
In my opinion, the diagram by R. Martin in Figure 1 can, without significant loss, be replaced by a more concise and pragmatic one β where the adapter layer serves as a bridge, depending both on the internal layers of the application and external components. This adjustment implies reversing the arrow from the blue layer to the green layer in R. Martin's diagram.
The proposed solution is a trade-off. It doesn't strictly follow R. Martin's original concept but avoids introducing excessive abstractions with implementations outside the application's boundaries. Pursuing purity on the outermost layer is more likely to result in overengineering than in practical gains.
My approach retains nearly all the advantages of Clean Architecture while simplifying real-world development. When needed, adapters can be removed along with the external components they're written for, which isn't a significant issue.
Let's agree, for this project, that Dependency Rule does not apply to adapters.
Figure 2: My Pragmatic Interpretation of Clean Architecture Diagram
(original and alternative representation)
Note
In the original diagram, the Presentation layer isn't explicitly distinguished and is instead included within the Interface Adapters layer. I chose to introduce it as a separate layer, marked in blue, as I see it as even more external compared to typical adapters.
- This layer handles external requests and includes controllers that validate inputs and pass them to the interactors in the Application layer. The more abstract layers of the program assume that data is already validated, allowing them to focus solely on their core logic.
- Controllers must be as thin as possible, containing no logic beyond basic input validation and routing. Their role is to act as an intermediary between the application and external systems (e.g., FastAPI).
Important
- Basic validation (e.g., type safety, required fields, input format) should be performed by controllers at this layer, while business rule validation (e.g., ensuring the email domain is allowed, verifying the uniqueness of username, or checking that a user meets the required age) belongs to the Domain layer.
- Domain validation often involves relationships between fields, such as ensuring that a discount applies only within a specific date range or a promotion code is valid for orders above a certain total.
- Pydantic is unsuitable for the Domain layer. Its parsing and serialization features have no relevance there, and while it might appear helpful for validation, it lacks the capabilities to handle complex relationships that may arise in Domain validation.
Note
In the original diagram, the external components are included in the blue layer (Frameworks & Drivers). I've marked them in gray to clearly distinguish them from the layers within the application's boundaries.
- This layer represents fully external components such as web frameworks (e.g. FastAPI itself), databases, third-party APIs, and other services.
- These components operate outside the applicationβs core logic and can be easily replaced or modified without affecting the business rules, as they interact with the application only through the Presentation and Infrastructure layers.
Figure 3: Basic Dependency Graph
The dependency inversion technique enables reversing dependencies by introducing an interface between components, allowing the inner layer to communicate with the outer layer while adhering to the Dependency Rule.
Figure 4: Corrupted Dependency
In this example, the Application component depends directly on the Infrastructure component, violating the Dependency Rule. This creates "corrupted" dependencies, where changes in the Infrastructure layer can propagate to and unintentionally affect the Application layer.
In the correct design, the Application layer component depends on an abstraction (port), and the Infrastructure layer component implements the corresponding interface. This makes the Infrastructure component an adapter for the port, effectively turning it into a plugin for the Application layer. Such a design adheres to the Dependency Inversion Principle (DIP), minimizing the impact of infrastructure changes on the core business logic.
The idea behind Dependency Injection is that a component shouldn't create the dependencies it needs but rather
receive them.
From this definition, it's clear that one common way to implement DI is by passing dependencies as arguments to the
__init__
method or functions.
But how exactly should these dependencies be passed?
DI frameworks offer an elegant solution by automatically creating the necessary objects (while managing their lifecycle) and injecting them where needed. This makes the process of dependency injection much cleaner and easier to manage.
Figure 6: Correct Dependency with DI
FastAPI provides a built-in DI mechanism called Depends, which tends to leak into different layers of the application. This creates tight coupling to FastAPI, violating the principles of Clean Architecture, where the web framework belongs to the outermost layer and should remain easily replaceable.
Refactoring the codebase to remove Depends
when switching frameworks can be unnecessarily costly. It also has other
limitations that are beyond the scope of
this README. Personally, I prefer Dishka β a solution that
avoids these issues and remains framework-agnostic.
An infrastructure "interactor" may be required as a temporary solution in cases where a separate context exists but isn't physically separated into a distinct domain (e.g., not implemented as a standalone module within a monolithic application). In such cases, the "interactor" operates as an application-level interactor but resides in the infrastructure layer.
In this application, such "interactors" include those managing user accounts, such as registration, login, and logout.
Identity Provider (IdP) abstracts authentication details, linking the main business context with the authentication context. In this example, the authentication context is not physically separated, making it an infrastructure detail. However, it can potentially evolve into a separate domain.
.
βββ ...
βββ .env.example # example env vars for Docker/local dev
βββ config.toml # primary config file
βββ Makefile # shortcuts for setup and common tasks
βββ pyproject.toml # tooling and environment config
βββ scripts/... # helper scripts
βββ src/
βββ app/
βββ run.py # app entry point
βββ application/... # application layer
β βββ common/ # common layer objects
β β βββ authorization/... # authorization logic
β β βββ ... # ports, exceptions, etc.
β βββ admin_create_user.py # interactor
β βββ ... # other interactors
βββ domain/ # domain layer
β βββ base/... # base declarations
β βββ user/... # user domain objects
βββ infrastructure/... # infrastructure layer
β βββ session_context/... # session-based auth context
β β βββ common/... # common context objects
β β βββ log_in.py # interactor
β β βββ ... # other interactors
β βββ ... # adapters, exceptions, etc.
βββ presentation/... # presentation layer
β βββ common/... # common layer objects
β βββ http_controllers/ # controllers (http)
β βββ user_change_password.py # controller
β βββ ... # other controllers
βββ setup/
βββ app_factory.py # app builder
βββ config/... # app settings
βββ ioc/... # dependency injection setup
- Python:
3.12
- Core:
alembic
,alembic-postgresql-enum
,bcrypt
,dishka
,fastapi
,orjson
,psycopg3[binary]
,pydantic[email]
,pyjwt[crypto]
,rtoml
,sqlalchemy[mypy]
,uuid6
,uvicorn
,uvloop
- Testing:
coverage
,pytest
,pytest-asyncio
- Development:
bandit
,black
,isort
,line-profiler
,mypy
,pre-commit
,pylint
,ruff
/
: Open to everyone.- Redirects to Swagger Documentation.
/api/v1/
: Open to everyone.- Returns
200 OK
if the API is alive.
- Returns
/signup
: Open to everyone.- Registers a new user with validation and uniqueness checks.
- Passwords are peppered, salted, and stored as hashes.
- A logged-in user cannot sign up until the session expires or is terminated.
/login
: Open to everyone.- Authenticates registered user, sets a JWT access token with a session ID in cookies, and creates a session.
- A logged-in user cannot log in again until the session expires or is terminated.
- Authentication renews automatically when accessing protected routes before expiration.
- If the JWT is invalid, expired, or the session is terminated, the user loses authentication. 1
/logout
: Open to authenticated users.- Logs the user out by deleting the JWT access token from cookies and removing the session from the database.
/
(POST): Open to admins.- Creates a new user, including admins, if the username is unique.
- Only super admins can create new admins.
/
(GET): Open to admins.- Retrieves a paginated list of existing users with relevant information.
/inactivate
: Open to admins.- Soft-deletes an existing user, making that user inactive.
- Also deletes the user's sessions.
- Only super admins can inactivate other admins.
/reactivate
: Open to admins.- Restores a previously soft-deleted user.
- Only super admins can reactivate other admins.
/grant
: Open to super admins.- Grants admin rights to a specified user.
/revoke
: Open to super admins.- Revokes admin rights from a specified user.
/change_password
: Open to authenticated users.- Changes the user's password.
- The current user can change their own password.
- Admins can change passwords of subordinate users.
Note
- The initial admin privileges must be granted manually (e.g., directly in the database), though the user account itself can be created through the API.
Warning
- This part of documentation is not related to the architecture approach.
You are free to choose whether to use the proposed automatically generated configuration system or provide your own
settings manually, which will require changes to the Docker configuration.
However, if settings are read from environment variables instead of
config.toml
, modifications to the application's settings code will be necessary.
Important
- In the configuration flow diagram below, the arrows represent the flow of data, not dependencies.
Figure 15: Configuration flow (.toml to .env)
config.toml
: primary config file.env
: derivative config file which Docker needs
Figure 16: Configuration flow (app)
Important
The following make
commands require Python >= 3.11
installed on your system.
Feel free to take a look at Makefile
, it contains many more useful commands.
- Set up development environment
# sudo apt update
# sudo apt install pipx
# pipx install uv
uv venv
source .venv/bin/activate
# .venv\Scripts\activate # Windows
# uv python install 3.12 # if you don't have it
uv pip install -e '.[test,dev]'
- Configure project
- Edit
config.toml
for primary configuration.
Warning
Don't rename existing variables or remove comments unless absolutely necessary.
This action may invalidate scripts associated with Makefile
.
You can still fix them or not use Makefile
at all.
- Generate
.env
file in one of the ways:-
Safe (as long as
config.toml
is correct)Set
POSTGRES_HOST
variable inconfig.toml
tolocalhost
, then:make dotenv
-
Convenient:
make dotenv-local
Automatically sets correct configuration
Under the hood, the corresponding variable in
config.toml
becomes uncommented, then the script associated withmake dotenv
is called. -
Manual (use with caution):
Rename
.env.example
to.env
and verify all variables.
-
- Launch
-
To run only the database in Docker and use the app locally, use the following command:
make up-local-db
-
Then, apply the migrations:
alembic upgrade head
-
After applying the migrations, you can start the application locally as usual. The database is now set up and ready to be used by your local instance.
- Shutdown
-
To stop the database container, use:
make down # or # docker compose down
-
To permanently delete the database along with the applied migrations, run:
make down-total # or # docker compose down -v
Important
The following make
commands require Python >= 3.11
installed on your system.
Feel free to take a look at Makefile
, it contains many more useful commands.
- Configure project
- Edit
config.toml
for primary configuration.
Warning
Don't rename existing variables or remove comments unless absolutely necessary.
This action may invalidate scripts associated with Makefile
.
You can still fix them or not use Makefile
at all.
- Generate
.env
file in one of the ways:-
Safe (as long as
config.toml
is correct)Set
POSTGRES_HOST
variable inconfig.toml
to the name of the Docker service fromdocker-compose.yaml
, then:make dotenv
-
Convenient:
make dotenv-docker
Automatically sets correct configuration
Under the hood, the corresponding variable in
config.toml
becomes uncommented, then the script associated withmake dotenv
is called. -
Manual (use with caution):
Rename
.env.example
to.env
and verify all variables.
-
- Launch
-
Choose one of the following commands:
-
make up # to run in detached mode # or # docker compose up --build -d
-
make up-echo # to run in non-detached mode # or # docker compose up --build
-
- Shutdown
-
To stop the containers, use:
make down # or # docker compose down
-
To completely remove the containers and permanently delete the database, run:
make down-total # or # docker compose down -v
-
Robert C. Martin. Clean Architecture: A Craftsman's Guide to Software Structure and Design. 2017
-
Alistair Cockburn. Hexagonal Architecture Explained. 2024 (introduced in 2005)
-
Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software. 2003
-
Martin Fowler. Patterns of Enterprise Application Architecture. 2002
I would like to express my sincere gratitude to the following individuals for their valuable ideas and support in satisfying my curiosity throughout the development of this project:
I would also like to thank all the other participants of the ASGI Community Telegram chat and βοΈ Reagento (adaptix/dishka) Telegram chat for their insightful discussions and shared knowledge.
- set up CI
- increase test coverage
- explain the code
Footnotes
-
Token and session share the same expiry time, avoiding database reads if the token is expired. β©