Clean Architecture
Concentric rings where Use Cases are king, ports make every boundary explicit, and nothing in the core ever touches the outside world
Introduction
Clean Architecture is a software design philosophy proposed by Robert C. Martin (Uncle Bob) in 2012. Like Onion Architecture, it organizes code into concentric rings with a strict inward-only dependency rule. At its core, it follows the same simple principle: high-level components must not depend on low-level components. Its central insight is that the Use Case ring, not the Entity ring, is the most important layer, because it expresses exactly what the application does.
What sets Clean Architecture apart from its siblings is the explicitness of its boundaries: Input Ports define how callers invoke a use case, and Output Ports define what a use case needs from the outside world. Every crossing of a layer boundary is a named interface, making the architecture self-documenting and every layer independently replaceable.
Quick Navigation
1. The Four Rings
Entities, Use Cases, Interface Adapters, Frameworks
2. Input and Output Ports
The contracts that make every boundary explicit
3. Key Concepts
Interactors, Gateways, DTOs, Composition Root
4. When to Use / Avoid
Fit criteria and anti-patterns
5. Pros and Cons
Trade-offs at a glance
6. vs. Onion and Hexagonal
Side-by-side comparison of the family
1. The Four Rings
Clean Architecture defines four concentric rings. The fundamental constraint is the same as in Onion Architecture: source-code dependencies can only point inward. An outer ring may import from any inner ring; an inner ring must never import from an outer one.
Clean Architecture: Ring Overview
Figure 1: All dependencies point inward. The Composition Root (Frameworks & Drivers) is the only place that touches all rings.
Entities (innermost)
Enterprise-wide business rules. These are the objects that would exist even if the application did not, they represent the core business domain.
- Aggregate roots - e.g.,
Orderwith status transition enforcement - Value Objects -
Money(frozen),OrderStatus(state machine) - Domain Exceptions -
InsufficientStockError,InvalidOrderTransitionError
Use Cases
Application-specific business rules. This ring is the heart of Clean Architecture, it defines exactly what the application does, expressed as named use cases.
- Input Ports - abstract interfaces defining use case entry points (
IPlaceOrderUseCase) - Output Ports - abstract interfaces for what use cases need (
IOrderGateway) - Interactors - concrete use case implementations (
PlaceOrderInteractor) - DTOs + Mappers -
PlaceOrderRequest,OrderResponse,_mappers.py
Interface Adapters
The translation zone. Converts data from the format used by Use Cases and Entities into the format expected by external tools, and vice versa.
- Gateways - implement Output Ports (
InMemoryOrderGatewayimplementsIOrderGateway) - Controllers / Presenters - translate HTTP requests into use case requests and responses back into HTTP responses
- No business logic - only format conversion and routing
Frameworks and Drivers (outermost)
The most volatile ring. Contains all external tools and the composition root that wires everything together.
- Database drivers - SQLAlchemy, psycopg2, motor
- Web frameworks - FastAPI, Django, Flask
- Composition root -
main.pyinstantiates and injects all concrete types
2. Input and Output Ports
Ports are the defining feature of Clean Architecture. Every crossing of a layer boundary is mediated by a named interface. This is what makes Clean Architecture the most explicit member of the inward-dependency family.
Input Ports: Driving the Application
An Input Port is an abstract interface that defines the contract any caller must satisfy to invoke a use case. It lives in the Use Cases ring. The caller (a controller, a CLI command, a task runner) depends on the Input Port interface, not on the concrete Interactor. This means you can swap the Interactor implementation without changing a single line of calling code.
# service/use_cases/ports/input/i_place_order_use_case.py
# Input Port: defines the contract any caller must use to invoke this use case.
# The task.py (Frameworks & Drivers) depends on this interface, NOT on
# the concrete PlaceOrderInteractor. This decouples the delivery mechanism
# from the implementation.
from abc import ABC, abstractmethod
from service.use_cases.dtos.order_dtos import OrderResponse, PlaceOrderRequest
class IPlaceOrderUseCase(ABC):
"""Input port that PlaceOrderInteractor must implement."""
@abstractmethod
def execute(self, request: PlaceOrderRequest) -> OrderResponse:
"""Place a new order; raise domain exceptions on validation failure."""Why it matters: In the demo, EcommerceOrderTask stores a reference typed as IPlaceOrderUseCase. In tests, you can inject a fake interactor that returns canned responses without touching any real business logic.
Output Ports: What the Use Case Needs
An Output Port is an abstract interface that the Use Case defines to express what it needs from the outside world. It also lives in the Use Cases ring, the use case owns the contract. Interface Adapters implement it. This is the key difference from Onion Architecture, where repository interfaces belong to the Domain ring.
# service/use_cases/ports/output/i_order_gateway.py
# Output Port: defines what THIS use case needs from the outside world.
# It lives in the Use Cases layer, the use case decides what it needs.
# Concrete implementations (InMemoryOrderGateway, PostgresOrderGateway)
# live in the Interface Adapters layer and implement this interface.
from abc import ABC, abstractmethod
from typing import List, Optional
from service.entities.order import Order
class IOrderGateway(ABC):
"""Output port for Order persistence. Concrete adapters live in interface_adapters."""
@abstractmethod
def save(self, order: Order) -> None: ...
@abstractmethod
def find_by_id(self, order_id: str) -> Optional[Order]: ...
@abstractmethod
def find_by_customer_id(self, customer_id: str) -> List[Order]: ...
@abstractmethod
def delete(self, order_id: str) -> None: ...The Interactor receives Output Port implementations through constructor injection:
# service/use_cases/interactors/place_order_interactor.py
# The Interactor is the concrete use case implementation.
# It implements the Input Port (IPlaceOrderUseCase) and
# depends ONLY on Output Ports and Entities, never on Interface Adapters.
class PlaceOrderInteractor(IPlaceOrderUseCase):
def __init__(
self,
order_gateway: IOrderGateway, # <- output port
product_gateway: IProductGateway, # <- output port
customer_gateway: ICustomerGateway,# <- output port
pricing_service: OrderPricingService, # <- use cases service
) -> None:
self._order_gateway = order_gateway
self._product_gateway = product_gateway
self._customer_gateway = customer_gateway
self._pricing = pricing_service
def execute(self, request: PlaceOrderRequest) -> OrderResponse:
customer = self._customer_gateway.find_by_id(request.customer_id)
if customer is None:
raise CustomerNotFoundError(...)
# Pass 1: validate stock for all items before mutating anything
resolved = []
for item_req in request.items:
product = self._product_gateway.find_by_id(item_req.product_id)
if not product.has_sufficient_stock(item_req.quantity):
raise InsufficientStockError(...)
resolved.append((product, item_req.quantity))
# Pass 2: all items validated, safe to decrement stock
order_items = []
for product, quantity in resolved:
product.decrement_stock(quantity)
order_items.append(OrderItem(...))
total = self._pricing.calculate_total(order_items)
order = Order(id=str(uuid.uuid4()), customer_id=customer.id, items=order_items)
order.set_total(total)
self._order_gateway.save(order)
return order_to_response(order)Why Output Ports belong in Use Cases, not Entities: The use case is the one that decides it needs find_by_customer_id(). Entities have no opinion about persistence, they are pure business objects. Moving the port to the Use Cases ring keeps Entities completely clean of any persistence knowledge.
3. Key Concepts
4. When to Use and When to Avoid
Use Clean Architecture when...
- Application business rules are complex and central - the Use Cases ring gives them a protected, first-class home with explicit named interfaces.
- Maximum replaceability is required - you anticipate swapping databases, frameworks, or external APIs over the system's lifetime.
- Multiple delivery mechanisms share the same logic - REST API, CLI, and queue consumer all depend on the same Input Ports.
- Explicit use-case contracts are a priority - Input Ports make the API of each use case a first-class artifact that teams can review and version.
- High testability at every layer - Interactors are testable with fake Output Port implementations; no real database or HTTP server needed.
Avoid Clean Architecture when...
- The app is mostly CRUD - Input Ports, Output Ports, Interactors, Gateways, and DTOs for every entity is heavy ceremony when there are no real business rules to protect.
- You want less ceremony than this - Onion Architecture delivers 90% of the same protection with less boilerplate; prefer it when explicit Input Ports are not needed.
- The team is new to the pattern - the terminology (interactor, gateway, port) and the extra interface layer require a period of adjustment.
- Short-lived prototypes - the structural investment pays back over a long application lifetime; for throwaway code it is overkill.
5. Pros and Cons
| Advantages | Trade-offs |
|---|---|
| Use cases are first-class citizens with explicit Input Port interfaces - the application's capabilities are self-documenting | Most verbose of the clean-architecture family: Input Ports + Output Ports + Interactors + Gateways + DTOs per use case |
| Entities are completely pure - no persistence contracts, no framework knowledge, zero external dependencies | Steeper learning curve than Onion Architecture due to additional port layer and different terminology |
| Swapping databases or external APIs requires only a new Gateway in Interface Adapters - entities and interactors are untouched | For simple CRUD the overhead of interfaces per boundary is significant ceremony with little payoff |
| Input Ports decouple delivery mechanisms from interactors - a new controller or queue consumer just depends on the port | Composition root grows in complexity proportionally with the number of use cases and gateways |
| Every layer is independently testable: entities with unit tests, interactors with fake gateways, adapters with integration tests | Terminology (interactor, gateway, port) can cause confusion in teams accustomed to standard repository or service naming |
6. Clean Architecture vs. Onion and Hexagonal
These three patterns enforce the same core rule (inward-only dependencies) but differ in where they draw layer boundaries, how explicit the ports are, and what terminology they use.
| Dimension | Clean Architecture | Onion Architecture | Hexagonal |
|---|---|---|---|
| Coined by | Uncle Bob, 2012 | Jeffrey Palermo, 2008 | Alistair Cockburn, 2005 |
| Ring / layer count | 4 explicit rings | 4 rings (Domain / App / Infra / Presentation) | No fixed ring count |
| Gateway interfaces live in | Use Cases ring (Output Ports) | Domain ring (Repository interfaces) | Inside the application hexagon |
| Input Ports (use-case interfaces) | Explicit ABCs per use case | Usually absent, concrete classes only | Explicit (driving ports) |
| Use case implementations called | Interactors | UseCases | Not standardized |
| Data-access implementations called | Gateways | Repositories | Adapters |
| Most central / protected ring | Use Cases | Domain (Entities) | Application core |
| Dependency inversion formalized via | Input + Output Ports (explicit) | Repository ABCs (partial) | Driving + driven ports (explicit) |
Clean Architecture
Maximum explicitness. Every layer boundary is a named interface. Output Ports in the Use Cases ring signal exactly what each use case needs from the outside world. Entities are the purest they can be, zero persistence knowledge. Choose this when explicit use-case contracts and maximum replaceability are the priority.
Onion Architecture
Same inward-dependency rule with less ceremony. Repository interfaces live in the domain, giving the domain ownership of its own persistence contracts. No Input Ports by default, use cases are concrete classes. Choose this when you want the same structural protections without the extra port layer.
Hexagonal Architecture
Emphasizes the diversity of entry and exit points: any number of driving adapters (REST, CLI, tests) and driven adapters (DB, email, queue). No fixed ring count, just an inner application core and an outer adapter layer. Choose this when the primary concern is supporting many interaction surfaces.
Practical guidance
In practice, teams often use the names interchangeably. All three enforce the same core rule and are architecturally equivalent at the conceptual level. If you see a codebase labeled "Hexagonal" with explicit Output Port interfaces and a composition root, it is functionally identical to Clean Architecture. Pick the name that resonates with your team, but enforce the dependency rule consistently.
Key Takeaways
- Use Cases are the most protected ring - not Entities. Clean Architecture places application behavior at the center of the design.
- Output Ports live in the Use Cases ring - the use case decides what it needs from the outside world; Entities have zero persistence knowledge.
- Input Ports make use cases explicitly callable - callers depend on the port interface, not on the concrete Interactor, enabling independent replacement.
- Interface Adapters are the translation zone - they convert between the format Use Cases need and the format external tools speak. No business logic belongs here.
- The composition root is the single wiring point - the only file that touches all rings simultaneously.
- Clean, Onion, and Hexagonal enforce the same rule - they differ in terminology and where they draw port boundaries. The dependency direction (always inward) is the invariant that matters.
Bonus: Working Code Demo
A complete Python implementation of Clean Architecture is available as a reference project. It models the same e-commerce order domain as the Onion Architecture demo, making a direct structural comparison possible between the two patterns.
The project includes:
- Entities ring -
Orderaggregate root,Moneyvalue object,OrderStatusstate machine, domain exceptions, zero external dependencies - Input Ports - one ABC per use case in
service/use_cases/ports/input/:IPlaceOrderUseCase,ICancelOrderUseCase,IGetOrderUseCase,IListCustomerOrdersUseCase - Output Ports -
IOrderGateway,IProductGateway,ICustomerGatewayinservice/use_cases/ports/output/ - Interactors - concrete use case implementations in
service/use_cases/interactors/includingPlaceOrderInteractorwith two-pass stock validation - Interface Adapters - in-memory gateway implementations in
service/interface_adapters/gateways/ - Composition root -
service/main.pywires all rings; run withpython manager.py run, tests withpython manager.py run-tests