Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 15

Clean Architecture: Enforcing the Dependency Rule

Recommended resources

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.

This diagram shows the four primary layers of Clean Architecture. The legend on the right maps these layers to more abstract categories: Enterprise Business Rules (core), Application Business Rules, Interface Adapters, and Frameworks & Drivers (details).

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 Order or User, are the nouns of your system and should be independent of any specific application. An Order entity, 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 PlaceOrder or RegisterUser. 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.

This sequence diagram shows the data flow for a client's GET request through all layers. It explicitly highlights where the Dependency Rule and Dependency Injection come into play.

Let's break down the steps shown in the diagram:

  1. Request Ingress (Infrastructure & Adapter): The client sends a GET /api/v1/cats/facts request. The web framework (Infrastructure) receives it and routes it to the GetAllCatFactsController (Adapter).

  2. 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.

  3. 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 axios or reqwest (Infrastructure) to call the external catfact.ninja API.
  4. Data Flowing Back (Infrastructure -> Adapter -> Application -> Adapter):

    • The external API returns raw data (e.g., JSON).
    • The HttpCatFactsRepository maps this raw data into an array of CatFactEntity objects (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.
  5. Response Egress (Adapter -> Infrastructure): The controller's final job is to prepare the response. It might use a Presenter to map the CatFactEntity array into the final JSON format sent to the client. The framework then handles sending the 200 OK response.

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 a BoundingBox, confidence_score, and class_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 an ImageFrame, returns a list of Detections. It depends on an abstract IObjectDetector interface.
    • 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 DetectObjectsInFrame use case.
    • Repository Implementations:
      • OnnxDetector: A class that implements the IObjectDetector interface using the ONNX runtime.
      • TensorRTDetector: An alternative implementation using NVIDIA's TensorRT.
      • PostgresAlertRepository: Implements an IAlertRepository interface to save critical events to a PostgreSQL database.
    • Presenters: An adapter that takes Detection entities and draws bounding boxes on an image for a debugging video stream.
  • Frameworks & Drivers:

    • The actual ONNX Runtime or TensorRT libraries.
    • The ffmpeg or gstreamer binaries/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 or Protocols), 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.