📘 This FastAPI-based project and its documentation represent a practical interpretation of Clean Architecture and Command Query Responsibility Segregation (CQRS) principles with elements of Domain-Driven Design (DDD). Although it's not meant to serve as a comprehensive reference or a strict application of these methodologies, the project demonstrates how their core ideas can be effectively put into practice in Python. If they're new to you, refer to the Useful Resources section.
If you find this project's structure useful but prefer starting with a clean state rather than deleting unnecessary code, I've created an interactive template for quickly generating a similar project setup: FastAPI Starter.
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 most abstract policies define core business rules, while the least abstract ones handle I/O operations. Being closer to implementation details, less abstract policies are more likely to change. A layer represents a collection of components expressing policies at the same level of abstraction.
The concentric circles represent boundaries between different layers. 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 reflects a change in 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.
- Domain services originally represent operations that don't naturally belong to a specific entity. In projects with anemic domain models — where entities hold data but no behavior — domain services may also include logic that would otherwise reside inside those entities.
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.
- Alternatively, application services provide a different way to organize interactors — placing them in a class per business context.
- Interactors are independent and should not call one another.
- The application layer may also include classes with non-orchestration business logic — e.g., authorization. These can also be considered services.
Note
Domain and Application layers may import external tools and libraries to the extent necessary for describing business logic — such as utilities for numerical computations, timezone management, or object modeling that extend the language's capabilities. 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 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.
-
Avoid letting business logic leak into peripheral details, such as raising business-specific exceptions in the Infrastructure layer without re-raising them in the business logic 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
UsernameAlreadyExistsError
for aUNIQUE CONSTRAINT
violation. Handling these exceptions in the Application layer ensures that any business logic implemented in adapters remains under control. -
Avoid introducing elements in inner layers that specifically exist to support outer layers. 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).
The Infrastructure layer in the Clean Architecture acts as the adapter layer — connecting the application to external systems. In this project, we treat both Infrastructure and Presentation as adapters, since both adapt the application to the outside world. Speaking of dependencies direction, 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
(diagrammed — 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 request 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, like checking whether the structure of the incoming request matches the structure of the defined request model (e.g., type safety and required fields) 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 if a user meets the required age) belongs to the Domain or Application layer.
- Business rule 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.
- Carefully consider using Pydantic for business rule validation. While convenient, Pydantic models are slower than regular dataclasses and reduce application core stability by coupling business logic to an external library.
- If you choose Pydantic (or a similar tool bundled with web framework) for business model definitions, ensure that a Pydantic model in business layers is a separate model from the one in the Presentation layer, even if their structure appears identical. Mixing data presentation logic with business logic is a common mistake made early in development to save effort on creating separate models and field mapping, often due to not understanding that structural similarities are temporary.
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 initialized (and finalized)?
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.
The project implements Command Query Responsibility Segregation (CQRS) — a pattern that separates read and write operations into distinct paths.
- Commands (via interactors) handle write operations and business-critical reads using command gateways that work with entities and value objects.
- Queries are implemented through query services (similar contract to interactors) that use query gateways to fetch data optimized for presentation as query models.
This separation enables:
- Efficient read operations through specialized query gates, avoiding loading complete entity models.
- Performance optimization by tailoring data retrieval to specific view requirements.
- Flexibility to combine data from multiple models in read operations with minimal field selection.
Application Controller - Interactor
Figure 7: Application Controller - Interactor
In the presentation layer, a Pydantic model appears when working with FastAPI and detailed information needs to be displayed in OpenAPI documentation. You might also find it convenient to validate certain fields using Pydantic; however, be cautious to avoid leaking business rules into the presentation layer.
For request data, a plain dataclass
is often sufficient.
Unlike lighter alternatives, it provides attribute access, which is more convenient for working in the application
layer.
However, such access is unnecessary for data returned to the client, where a TypedDict
is sufficient (it's
approximately twice as fast to create as a dataclass with slots, with comparable access times).
Infrastructure Controller - Handler
Figure 11: Infrastructure Controller - Handler
An infrastructure handler 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 handler operates as an application-level interactor but resides in the infrastructure layer.
Initially, I called these handlers interactors, but the community reacted very negatively to the idea of interactors in the infrastructure layer, refusing to acknowledge that these essentially belong to another context.
In this application, such handlers include those managing user accounts, such as registration, login, and logout.
Infrastructure Handler
Figure 12: Infrastructure Handler
Ports in infrastructure are not commonly seen — typically, only concrete implementations are present. However, in this project, since we have a separate layer of adapters (presentation) located outside the infrastructure, ports are necessary to comply with the dependency rule.
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.
Identity Provider
Normally, IdP is expected to provide all information about current user. However, in this project, since roles are not stored in sessions or tokens, retrieving them in main context was more natural.
.
├── config/... # configuration files and scripts, includes Docker
├── Makefile # shortcuts for setup and common tasks
├── scripts/... # helper scripts
├── pyproject.toml # tooling and environment config (uv)
├── ...
└── src/
└── app/
├── run.py # app entry point
│
├── application/... # application layer
│ ├── common/ # common layer objects
│ │ ├── services/... # authorization, etc.
│ │ └── ... # ports, exceptions, etc.
│ │
│ ├── commands/ # write operations, business-critical reads
│ │ ├── admin_create_user.py # interactor
│ │ └── ... # other interactors
│ │
│ └── queries/ # optimized read operations
│ ├── admin_list_users.py # query service
│ └── ... # other query services
│
├── domain/ # domain layer
│ ├── entities/... # entities (have identity)
│ │ ├── base.py # base declarations
│ │ └── ... # concrete entities
│ │
│ ├── services/... # domain layer services
│ │
│ ├── value_objects/... # value objects (no identity)
│ │ ├── base.py # base declarations
│ │ └── ... # concrete value objects
│ │
│ └── ... # ports, enums, exceptions, etc.
│
├── infrastructure/... # infrastructure layer
│ ├── auth_context/... # auth context (session-based)
│ │ ├── common/... # common context objects
│ │ │
│ │ ├── log_in.py # handler
│ │ └── ... # other handlers
│ │
│ └── ... # 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
If you find this structure useful but prefer starting with a clean state rather than deleting unnecessary code, I've created an interactive template for quickly generating a similar project setup: FastAPI Starter.
- Python:
3.12
- Core:
alembic
,alembic-postgresql-enum
,bcrypt
,dishka
,fastapi
,orjson
,psycopg3[binary]
,pydantic[email]
,pyjwt[crypto]
,rtoml
,sqlalchemy[mypy]
,uuid6
,uvicorn
,uvloop
- Development:
mypy
,pre-commit
,ruff
- Testing:
coverage
,line-profiler
,pytest
,pytest-asyncio
/
: 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.
- Super admins cannot be soft-deleted.
/reactivate
: Open to admins.- Restores a previously soft-deleted user.
- Only super admins can reactivate other admins.
- Super admins cannot be soft-deleted.
/grant
: Open to super admins.- Grants admin rights to a specified user.
- Super admin rights can not be changed.
/revoke
: Open to super admins.- Revokes admin rights from a specified user.
- Super admin rights can not be changed.
/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
- Super admin privileges must be initially 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.
- Use any configuration method you prefer.
- config.toml: Main application settings organized in sections
- export.toml: Lists fields to export to .env (
export.fields = ["postgres.USER", "postgres.PASSWORD", ...]
) - .secrets.toml: Optional sensitive data (same format as config.toml, merged with main config)
Important
- This project includes secret files for demonstration purposes only. In a real project, you must ensure that
.secrets.toml
and all.env
files are not tracked by version control system to prevent exposing sensitive information. See this project's.gitignore
for an example of how to properly exclude these sensitive files from Git.
In this project I use my own configuration system based on TOML files as the single source of truth.
The system generates .env
files for Docker and infrastructure components while the application reads settings directly
from the structured TOML files. More details are available at https://github.com/ivan-borovets/toml-config-manager
Figure 15: Configuration flow
Here, the arrows represent usage flow, not dependencies.
- Configure local environment
- Create
.secrets.toml
inconfig/local
following.secrets.toml.example
- Edit TOML files in
config/local
according to your project requirements - When using Docker Compose, remember to pass
APP_ENV
to your service:
services:
app:
# ...
environment:
APP_ENV: ${APP_ENV}
.env.local
will be generated later — don't create it manually
- Set environment variable
export APP_ENV=local
# export APP_ENV=dev
# export APP_ENV=prod
- Check it and generate
.env
# Probably you'll need Python 3.12 installed on your system to run these commands.
# The next code section provides commands for its fast installation.
make env # should print APP_ENV=local
make dotenv # should tell you where .env.local was generated
- Set up virtual environment
# sudo apt update
# sudo apt install pipx
# pipx ensurepath
# pipx install uv
# uv python install 3.12
uv v
source .venv/bin/activate
# .venv\Scripts\activate # Windows
# https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion
uv pip install -e '.[dev,test]'
Don't forget to tell your IDE where the interpreter is located.
Install pre-commit hooks:
# https://pre-commit.com/
pre-commit install
- Launch
-
To run only the database in Docker and use the app locally, use the following command:
make up.db # make up.db-echo
-
Then, apply the migrations:
alembic upgrade head
-
After applying the migrations, the database is ready, and you can launch the application locally (e.g., through your IDE). Remember to set the
APP_ENV
environment variable in your IDE's run configuration. -
To run via Docker Compose:
make up # make up.echo
In this case, migrations will be applied automatically at startup.
- Shutdown
- To stop the containers, use:
make down
- Use the instructions about local environment above
- But make sure you've created similar structure in
config/dev
orconfig/prod
with files:config.toml
.secrets.toml
export.toml
docker-compose.yaml
if needed
.env.dev
or.env.prod
to be generated later — don't create them manually
- Add new value to
ValidEnvs
enum inconfig/toml_config_manager.py
(and maybe in your app settings) - Update
ENV_TO_DIR_PATHS
mapping in the same file (and maybe in your app settings) - Create corresponding directory in
config/
folder - Add required configuration files
Environment directories can also contain other env-specific files like docker-compose.yaml
, which will be used by
Makefile commands.
-
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
If you find this project useful, please give it a star or share it! Your support means a lot.
💬 Feel free to open issues, ask questions, or submit pull requests.
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: igoryuha, tishka17, chessenjoyer17, PlzTrustMe, Krak3nDev, Ivankirpichnikov, nkhitrov, ApostolFet, Lancetnik, Sehat1137, Maclovi.
I also greatly appreciate the valuable insights shared by participants of the ASGI Community Telegram chat, despite frequent and lively communication challenges, as well as the ⚗️ Reagento (adaptix/dishka) Telegram chat for their thoughtful discussions and generous knowledge exchange.
- set up CI
- simplify settings
- simplify annotations
- increase test coverage
- explain code
Footnotes
-
Session and token share the same expiry time, avoiding database reads if the token is expired. This scheme of using JWT is not related to OAuth 2.0 and is a custom micro-optimization. ↩