Introduction
In our last lesson, we explored Hexagonal Architecture, a pattern designed to isolate your application's core logic from external technologies using "ports" and "adapters." We focused on how this "inside-out" approach creates a clear boundary, ensuring the core depends on nothing. Today, we delve into Clean Architecture, a concept formalized by Robert "Uncle Bob" Martin that you will find is a close cousin to the hexagonal style.
This lesson will feel like a natural continuation, as Clean Architecture shares the same fundamental goal: creating systems that are independent of frameworks, databases, and UIs. We will formalize the structure inside the hexagon by defining a specific set of concentric layers—Entities, Use Cases, Interface Adapters, and Frameworks. Your objective is to learn how to design an application according to these layers, ensuring that all dependencies adhere strictly to the central Dependency Rule.
The Layers of Clean Architecture
Clean Architecture organizes a system into a series of concentric circles, often visualized as an onion. Each circle represents a different level of software, from the most abstract business rules at the center to the most concrete implementation details on the outside.

The most important principle governing this structure is the Dependency Rule: source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. This means that a change in an outer layer, such as swapping your web framework or database, will not require any changes to the inner layers.
Let's break down the responsibility of each layer.
Clean Architecture Essentials: Transforming Python ...
This article provides an excellent breakdown of the four layers of Clean Architecture. It clearly defines the role of each layer and provides examples that will help solidify your understanding.
Please read the section titled The onion architecture concept. As you read, focus on building a mental model for what kind of code belongs in each of the four layers: Entities: What makes them "enterprise-wide"? Use Cases: How are they different from the business rules in Entities? Interface Adapters: What role do they play as "translators"? Frameworks and Drivers: Why are these considered "details"?
To summarize and build on that reading:
- Entities (Domain Layer): This is the core. It contains the fundamental business objects and the rules that govern them. These objects, like
OrderorUser, are the nouns of your system and should be independent of any specific application. AnOrderentity, for example, might have a rule that its total value can never be negative, regardless of whether it's used in a web shop or a backend processing system. - Use Cases (Application Layer): This layer contains application-specific business logic. A use case orchestrates the flow of data to and from the entities to achieve a specific application goal, like
PlaceOrderorRegisterUser. It defines the inputs required and the outputs produced but remains completely ignorant of how the data is delivered (e.g., HTTP request) or persisted (e.g., SQL database). - Interface Adapters: This layer acts as a set of converters. It takes data from the format most convenient for the Use Cases and Entities and transforms it into the format most convenient for external systems like a database or a web UI, and vice versa. This layer includes:
- Controllers/Gateways: Which handle incoming requests from the outside world.
- Presenters: Which format the output from the Use Cases for display.
- Repositories: Which provide an abstract interface for data persistence.
- Frameworks & Drivers: This is the outermost layer, composed of all the concrete technologies: web frameworks (FastAPI, Axum), database drivers (psycopg2, sqlx), UI frameworks, etc. This layer is the most volatile and contains the "glue" code that wires everything together.
The Dependency Rule and Dependency Inversion
You might be asking: if the Use Case layer can't depend on the Frameworks layer, how does a use case like RegisterUser save a user to a database? The answer lies in the Dependency Inversion Principle, which we touched on in Module 1 and saw in action with Hexagonal Architecture.
The inner layer dictates the contract. Instead of the Use Case depending on a concrete database repository, it depends on an abstract interface (or "port") that it defines itself. The outer layer then provides a concrete implementation of that interface.
The flow of control may go from the Use Case to the database, but the source code dependency points from the database implementation inward to the interface defined by the Use Case.
This video provides an excellent animated walkthrough of this entire flow.
Clean Architectures in Python - presented by Leonardo Giordani
This segment from Leonardo Giordani's talk on "Clean Architectures in Python" visually demonstrates the flow of a request and the application of the Dependency Rule.
Watch from the example, which traces a request to retrieve a list of items. Pay close attention to how the Use Case communicates with the Database via an Interface, and how data is converted between layers (Entities, JSON). This visualization makes the abstract rule very concrete.
A Practical Data Flow Example
Let's trace a GET request through a simple "Cat Facts" API built with Clean Architecture. This will solidify the interaction between the layers. The diagram below illustrates the entire process.

Let's break down the steps shown in the diagram:
-
Request Ingress (Infrastructure & Adapter): The client sends a
GET /api/v1/cats/factsrequest. The web framework (Infrastructure) receives it and routes it to theGetAllCatFactsController(Adapter). -
Use Case Execution (Adapter -> Application): The controller invokes the
GetAllCatFactsUseCase(Application). Critically, it injects a concrete repository implementation that satisfies the repository interface required by the use case. -
Data Fetching (Application -> Adapter -> Infrastructure):
- The use case calls the
get_all_cat_facts()method on the abstract repository interface it depends on. - The flow of control passes to the concrete
HttpCatFactsRepository(Adapter), which implements this interface. - The repository uses an HTTP client like
axiosorreqwest(Infrastructure) to call the externalcatfact.ninjaAPI.
- The use case calls the
-
Data Flowing Back (Infrastructure -> Adapter -> Application -> Adapter):
- The external API returns raw data (e.g., JSON).
- The
HttpCatFactsRepositorymaps this raw data into an array ofCatFactEntityobjects (Domain objects). - It returns this array of entities back to the
GetAllCatFactsUseCase. - The use case, having completed its job, returns the entities to the
GetAllCatFactsController.
-
Response Egress (Adapter -> Infrastructure): The controller's final job is to prepare the response. It might use a
Presenterto map theCatFactEntityarray into the final JSON format sent to the client. The framework then handles sending the200 OKresponse.
This example clearly shows how the core logic in the Application layer orchestrates the task without having any dependency on FastAPI, Axum, or the reqwest library.
Implementing Clean Architecture in Python and Rust
The key to enforcing the Dependency Rule at layer boundaries is defining abstract interfaces. As you know from your experience, Python and Rust provide excellent tools for this.
Python: Protocols and ABCs
In Python, you can use typing.Protocol for structural subtyping (duck typing with type hints) or abc.ABC for nominal subtyping (explicit inheritance). Both are effective at defining the abstract interfaces required by the inner layers.
Clean Architecture Essentials: Transforming Python ...
This article section provides a clear, practical example of implementing the Dependency Rule in Python.
Read the section on implementation. It demonstrates how a NotificationService (Application layer) can depend on an abstract Notifier interface, with concrete EmailNotifier and SMSNotifier classes (Adapter/Infrastructure layers) providing the implementation. Pay attention to the comparison between the ABC and Protocol approaches.
Rust: Traits
In Rust, traits are the idiomatic and compile-time enforced way to define these boundaries. A use case can depend on a dyn Trait object, which is provided by the outer layers at runtime.
This video provides a complete walkthrough of a Rust project template that is structured according to Clean Architecture.
Practical Clean Architecture in Rust [with Axum Template]
The channel "Green Tea Coding" offers a superb practical guide to Clean Architecture in Rust. This video explains how to use traits as ports and then walks through a full Axum-based project template.
First, watch the section on dependency inversion. This explains precisely how a trait defined in the Application layer acts as a "port" to invert the dependency on the persistence adapter. Next, watch the project walkthrough from real code. This will show you a tangible project structure with domain, application, adapters, and infrastructure directories. Focus on how: The application/user/use_cases.rs file defines traits for persistence and hashing. The adapters/persistence/postgres.rs file implements the persistence trait. The infrastructure/setup.rs file performs dependency injection, wiring the concrete PostgresPersistence adapter into the UserUseCases struct.
Application to a Computer Vision Pipeline
Let's revisit the computer vision pipeline concept and apply the more detailed layers of Clean Architecture. This will clarify the separation of concerns in such a complex system.
-
Entities:
ImageFrame: Contains image data and metadata (timestamp, source). Rule: timestamp cannot be in the future.Detection: Represents a detected object with aBoundingBox,confidence_score, andclass_label. Rule: confidence must be between 0 and 1.- These are pure, self-validating data structures, independent of OpenCV, PyTorch, or any other library.
-
Use Cases:
DetectObjectsInFrame: Takes anImageFrame, returns a list ofDetections. It depends on an abstractIObjectDetectorinterface.TrackObjects: Takes a series of frames and detections, returns object tracks with stable IDs.- This layer orchestrates the core CV tasks but doesn't know how detection is performed (e.g., YOLOv8 vs. Faster R-CNN) or where frames come from.
-
Interface Adapters:
- Controllers/Gateways: An adapter that listens to an RTSP stream, decodes frames, and calls the
DetectObjectsInFrameuse case. - Repository Implementations:
OnnxDetector: A class that implements theIObjectDetectorinterface using the ONNX runtime.TensorRTDetector: An alternative implementation using NVIDIA's TensorRT.PostgresAlertRepository: Implements anIAlertRepositoryinterface to save critical events to a PostgreSQL database.
- Presenters: An adapter that takes
Detectionentities and draws bounding boxes on an image for a debugging video stream.
- Controllers/Gateways: An adapter that listens to an RTSP stream, decodes frames, and calls the
-
Frameworks & Drivers:
- The actual ONNX Runtime or TensorRT libraries.
- The
ffmpegorgstreamerbinaries/libraries used for stream decoding. - The PostgreSQL database server and its driver (
sqlx). - A web server (e.g., Axum) that exposes an endpoint to view live results.
With this structure, you could switch from an ONNX model to a TensorRT model simply by changing one line in your dependency injection setup (the "Composition Root"), without altering a single line of code in your core DetectObjectsInFrame use case. This makes your CV logic exceptionally robust, testable, and maintainable.
Conclusion
Today we've formalized the principles of isolated application cores by studying Clean Architecture. You've seen that it's not a radical departure from Hexagonal Architecture but rather a more detailed prescription for structuring the layers of your application.
Key Takeaways:
- Clean Architecture organizes a system into four primary layers: Entities, Use Cases, Interface Adapters, and Frameworks & Drivers.
- The Dependency Rule is the guiding principle: all source code dependencies must point inwards, toward the Entities.
- The core business logic (Entities and Use Cases) is completely independent of external concerns like databases, web frameworks, or hardware.
- Dependency Inversion is the key mechanism for adhering to the Dependency Rule. Inner layers define abstract interfaces (
traits orProtocols), and outer layers provide the concrete implementations. - This architecture produces systems that are highly testable, maintainable, and flexible to changing technical requirements.
In our next lesson, we will focus specifically on the techniques for creating the seams between these layers. We will take a deep dive into defining boundaries between layers or components using abstract interfaces, examining the nuances of Python's Protocols and Rust's traits to design robust and clean contracts in your code.