Skip to main content

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​

LayerContains
Entitiescore domain rules, framework-agnostic
Use Casesapp-specific orchestration via ports
Interface Adapterscontrollers, presenters, repo impls
Frameworks & DriversDB, 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-linter contract rules (domain can't import infrastructure)
  • Go: internal/ packages, go-cleanarch lint
  • 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
See: Common Mistakes

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.