Clean Architecture cheatsheet
A one-page reference for Clean Architecture. For the worked Python and Go examples and the full DDD-lite discussion, see the complete guide.
π Full guide: Clean Architecture βThe Dependency Ruleβ
Source code dependencies must point only inward. Nothing in an inner circle can know anything about something in an outer circle.
Entities never import a DB driver; a DB adapter always imports the port an inner layer defined.
The four layersβ
| Layer | Contains |
|---|---|
| Entities | core domain rules, framework-agnostic |
| Use Cases | app-specific orchestration via ports |
| Interface Adapters | controllers, presenters, repo impls |
| Frameworks & Drivers | DB, web framework, UI β the details |
Ports & adaptersβ
compile-time: Adapter ββdepends onβββΆ Port (interface)
runtime flow: UseCase ββcallsβββΆ Adapter (via the port)
The use case defines the port; the outer layer implements it. Source dependency points inward even though control flows outward.
Port (domain-owned interface)β
class OrderRepository(ABC):
@abstractmethod
def get(self, order_id: UUID) -> Order | None: ...
@abstractmethod
def save(self, order: Order) -> None: ...
Use case (depends only on ports)β
class PlaceOrderUseCase:
def __init__(self, orders: OrderRepository, inventory: InventoryChecker):
self._orders = orders # port, not concrete adapter
self._inventory = inventory
def execute(self, request: PlaceOrderRequest) -> PlaceOrderResponse:
...
Two adapters, one portβ
class PostgresOrderRepository(OrderRepository):
... # real infrastructure
class InMemoryOrderRepository(OrderRepository):
... # test fake, same interface
Payoff: the use case runs fully, with zero infrastructure, in microseconds under test.
Composition rootβ
# main.py β the ONLY file that imports both domain and a concrete adapter
use_case = PlaceOrderUseCase(
orders=PostgresOrderRepository(conn),
inventory=StockServiceInventoryChecker(base_url="..."),
)
Swap PostgresOrderRepository for DynamoOrderRepository and nothing in
domain/ or application/ changes.
Hexagonal & Onion = same ideaβ
- Hexagonal (Cockburn): core = Entities+Use Cases; driving adapters call in, driven adapters get called out to.
- Onion (Palermo): concentric rings, Domain Model at center.
- Same Dependency Rule, different vocabulary β don't get hung up on "port" vs "interactor," get hung up on whether domain imports the DB driver.
Enforce it structurally, not by conventionβ
- Python:
import-lintercontract rules (domain can't import infrastructure) - Go:
internal/packages,go-cleanarchlint - Java/Kotlin: ArchUnit package-dependency tests
Treat a forbidden import as a build failure, not a review nitpick.
Common mistakesβ
- Anemic ORM model standing in for the domain entity
- Use case taking a framework request object directly (leaking DTOs)
- Fat repository ports shaped by the DB, not by what the use case needs
- Applying full 4-layer Clean Architecture to a 5-endpoint CRUD app
When to actually reach for itβ
Genuine domain complexity, multiple/uncertain infra choices, or a team large enough that decoupling reduces merge conflicts β not by default on every project.