backend / architecture design / 09_clean_architecture.md

Clean Architecture

7 interview angles 8 min read source

Clean Architecture

Robert C. Martin’s name for an architecture style that combines and refines Hexagonal (Cockburn, 2005), Onion (Palermo, 2008), and DDD ideas. Published in Clean Architecture (2017). The key innovation: a strict dependency rule (all dependencies point inward) and explicit named layers (Entities, Use Cases, Interface Adapters, Frameworks).

For Hexagonal (closely related) see 11_hexagonal_ports_adapters.md. For general layered architecture see 06_layered_architecture.md.

The diagram

┌──────────────────────────────────────────────────────────┐
│         Frameworks & Drivers                              │
│  (HTTP, DB, UI, devices — anything external)              │
│  ┌────────────────────────────────────────────────────┐   │
│  │   Interface Adapters                                │   │
│  │  (controllers, gateways, presenters)                │   │
│  │  ┌────────────────────────────────────────────┐    │   │
│  │  │   Application Business Rules                │    │   │
│  │  │   (Use Cases — orchestrate entities)         │    │   │
│  │  │  ┌──────────────────────────────────┐        │    │   │
│  │  │  │  Enterprise Business Rules        │        │    │   │
│  │  │  │  (Entities — corporate-wide rules)│        │    │   │
│  │  │  └──────────────────────────────────┘        │    │   │
│  │  └────────────────────────────────────────────┘    │   │
│  └────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────┘

Dependency Rule: dependencies point inward only.

Each ring depends only on rings closer to the center. The innermost ring (Entities) has no dependencies. The outermost (Frameworks) depends on everything.

The four rings

Ring Contents Knows about
Entities corporate-wide business rules and data nothing
Use Cases application-specific business rules Entities
Interface Adapters convert data between Use Cases and external systems Use Cases, Entities
Frameworks & Drivers DB, Web, UI, external APIs Interface Adapters

Entities

Pure business objects. Behavior + data. No framework dependencies.

@dataclass
class Account:
    id: AccountId
    balance: Decimal
    status: AccountStatus

    def withdraw(self, amount: Decimal) -> None:
        if self.status != AccountStatus.ACTIVE:
            raise AccountClosedError
        if amount > self.balance:
            raise InsufficientFundsError
        self.balance -= amount

    def deposit(self, amount: Decimal) -> None:
        if amount <= 0:
            raise InvalidAmountError
        self.balance += amount

These rules (“can’t withdraw from closed accounts”) would apply even if the app were rewritten in another language with a different DB. They’re enterprise-wide.

No imports of SQLAlchemy, Flask, FastAPI, requests, etc. The entity is testable in pure Python.

Use Cases

Application-specific orchestration. Coordinate entities + repositories + external services.

class TransferMoneyUseCase:
    def __init__(self, accounts: AccountRepository, ledger: LedgerService):
        self.accounts = accounts
        self.ledger = ledger

    def execute(self, from_id, to_id, amount):
        with self.accounts.transaction():
            from_acc = self.accounts.get(from_id)
            to_acc = self.accounts.get(to_id)
            from_acc.withdraw(amount)
            to_acc.deposit(amount)
            self.accounts.save(from_acc)
            self.accounts.save(to_acc)
            self.ledger.record_transfer(from_id, to_id, amount)

The use case knows what “transferring money” means in this application. It depends on abstractions (AccountRepository, LedgerService) — defined in the Use Cases layer, implemented in Frameworks.

Interface Adapters

Convert formats between Use Cases and the outside world.

  • Controllers: convert HTTP requests → use case inputs.
  • Presenters: convert use case outputs → response DTOs.
  • Gateways: convert use case calls → external API calls.
class TransferMoneyController:
    def __init__(self, use_case: TransferMoneyUseCase):
        self.use_case = use_case

    def handle(self, request):
        try:
            self.use_case.execute(
                from_id=AccountId(request.json["from"]),
                to_id=AccountId(request.json["to"]),
                amount=Decimal(request.json["amount"]),
            )
            return {"status": "ok"}, 200
        except InsufficientFundsError:
            return {"error": "insufficient_funds"}, 400

This layer is where framework types meet domain types. The controller speaks HTTP; the use case speaks domain.

Frameworks & Drivers

Concrete implementations. Flask, SQLAlchemy, Redis, third-party APIs. Replaceable.

class SqlAlchemyAccountRepository:
    def __init__(self, session):
        self.session = session

    def get(self, id):
        row = self.session.get(AccountModel, id)
        return Account(id=row.id, balance=row.balance, status=row.status)

    def save(self, account):
        self.session.merge(AccountModel.from_domain(account))

Implements the AccountRepository interface that the Use Case depends on. The use case doesn’t know it’s SQLAlchemy.

The Dependency Rule

Source code dependencies point only inward. Inner circles never know about outer circles.

# entities/account.py
class Account: ...
# NO imports of anything else in the project

# use_cases/transfer.py
from entities.account import Account
from use_cases.ports import AccountRepository      # interface defined here
# NO imports of frameworks or adapters

# adapters/repositories.py
from use_cases.ports import AccountRepository
from frameworks.db import sqlalchemy_session

class SqlAlchemyAccountRepository(AccountRepository):  # implements the inner interface
    ...

# frameworks/web.py
from use_cases.transfer import TransferMoneyUseCase
from adapters.repositories import SqlAlchemyAccountRepository
# composition root: wires concrete implementations to use cases

Compile-time: outer depends on inner. Runtime: data flows in both directions, but the static dependency arrows only point inward.

This is dependency inversion: the use case defines an interface for “what it needs”; the framework implements it.

Why the strict rule?

  • Independence of frameworks: switch Flask → FastAPI; only the outermost ring changes.
  • Testability: domain testable without DB / HTTP / mocks. Pure Python.
  • Independence of UI: web, CLI, mobile — different presentations of the same use cases.
  • Independence of DB: switch Postgres → MongoDB; only repositories change.

The trade-off: significant up-front structure. Worth it for complex domains; overkill for CRUD.

Compared to other styles

Clean Hexagonal Onion Layered (classic)
Dependency direction strictly inward “ports” on the outside strictly inward top-down
Entity layer yes yes yes implicit (or absent)
Explicit use case layer yes yes yes (app services) sometimes
Adapter layer yes yes (“adapters”) yes (infrastructure) “data access layer”
Domain → DB allowed? no no no yes (classic)

Clean and Onion are nearly identical. Hexagonal is the same idea framed around “ports and adapters.” Layered (classic) is the predecessor — Clean fixes the “domain depends on DB” problem via dependency inversion.

Python structure example

myapp/
├── domain/                          # Entities
│   ├── account.py
│   ├── transfer.py
│   └── exceptions.py
├── application/                     # Use Cases
│   ├── ports.py                     # repository interfaces, service interfaces
│   ├── transfer_use_case.py
│   ├── deposit_use_case.py
│   └── close_account_use_case.py
├── adapters/                        # Interface Adapters
│   ├── http/                        # controllers, presenters
│   │   ├── routes.py
│   │   └── schemas.py
│   └── repositories/                # gateway implementations
│       ├── sqlalchemy_account_repo.py
│       └── redis_session_repo.py
├── infrastructure/                  # Frameworks & Drivers
│   ├── db.py
│   ├── webapp.py
│   └── config.py
└── main.py                          # composition root — wires it all together

main.py is the only file that imports concrete implementations and injects them into use cases. Everywhere else, code depends on abstractions.

Composition root

The one place that knows about everything:

# main.py
from infrastructure.db import create_session
from adapters.repositories.sqlalchemy_account_repo import SqlAlchemyAccountRepository
from application.transfer_use_case import TransferMoneyUseCase
from adapters.http.routes import TransferMoneyController

def build_app():
    session = create_session()
    accounts = SqlAlchemyAccountRepository(session)
    transfer_uc = TransferMoneyUseCase(accounts)
    controller = TransferMoneyController(transfer_uc)
    return controller

For larger apps: use a DI container (dependency-injector library) or simple factory functions. See 03_dependency_injection.md.

When Clean Architecture pays off

  • Complex business logic worth isolating from framework churn.
  • Long-lived applications where you’ll outlast multiple frameworks.
  • Multiple entry points: web + CLI + queue consumers all using the same use cases.
  • Team size large enough that “where does X go?” benefits from rigid rules.
  • Heavy testing requirements: pure-Python domain tests run fast.

When it doesn’t

  • CRUD apps: layers add ceremony without value. Django’s MTV is fine.
  • Prototypes / spikes: speed beats purity.
  • Tiny services: 200-line microservices with one endpoint don’t need layers.
  • Teams unfamiliar with the pattern: rolling out Clean Architecture without buy-in causes inconsistent half-application.

Anti-patterns

“Anemic” Clean Architecture

Entities are data bags; all logic in use cases. Domain rules (“can’t withdraw from closed account”) leak into application layer. Fix: move logic onto entities.

Use cases per endpoint

CreateOrderUseCase, GetOrderUseCase, UpdateOrderUseCase, DeleteOrderUseCase, ListOrdersUseCase

Effectively service-per-method. Often a class with a single execute method is just a function with extra ceremony.

Pragmatic alternative: one OrderService with several methods. Some Clean Architecture purists object; in practice it’s fine.

Dual mapping overhead

Domain Account → ORM AccountModel → response DTO AccountResponse. Every read involves three representations. Tedious for trivial fields.

Mitigation: code generation (Pydantic, dataclasses) for the boilerplate. Accept the cost for the testability benefit.

Premature abstraction

Building Clean Architecture for a 3-month MVP. Most of the abstractions are never swapped out. The “framework independence” benefit doesn’t materialize.

Start simpler. Refactor toward Clean when complexity demands it.

Common interview confusions

  • “Clean Architecture is the same as MVC.” — MVC is a presentation pattern; Clean Architecture is a whole-application structure. MVC may live within the outer ring.
  • “Clean Architecture means lots of folders.” — the file layout is incidental. The rule is dependencies point inward; you can implement that in many directory structures.
  • “You need Clean Architecture for any real project.” — overkill for many. CRUD apps, simple services, prototypes don’t benefit.

Interview angle

  • “What is Clean Architecture?” — Uncle Bob’s name for an architecture style with four concentric rings (Entities, Use Cases, Interface Adapters, Frameworks) and a strict dependency rule: all dependencies point inward. Domain knows nothing of the framework.
  • “What’s the dependency rule?” — source code dependencies point only inward. Inner rings can’t import from outer rings. The Use Case defines what it needs as an interface; the Framework layer implements it. Standard dependency inversion.
  • “Clean Architecture vs Hexagonal Architecture?” — same shape, different framing. Hexagonal calls the boundaries “ports” (interfaces) and “adapters” (implementations); Clean adds explicit Entities (enterprise rules) and Use Cases (application rules) layers. Effectively interchangeable in practice.
  • “What’s the difference between Entities and Use Cases?” — Entities encode enterprise-wide business rules (would apply even in a different application). Use Cases encode application-specific orchestration (specific to this app’s workflows).
  • “When does Clean Architecture pay off?” — complex domains with long-lived apps, multiple entry points (web + CLI + queue), high testability requirements, large teams. Not for CRUD apps, prototypes, or small services.
  • “What’s the ‘anemic domain’ anti-pattern?” — entities with no behavior (just data); all logic in services. Fix: move methods onto entities. Symptom that “I have layers but no real domain layer.”
  • “What’s a composition root?” — the one place that wires concrete implementations together (creates DB sessions, instantiates repositories, injects them into use cases). Usually main.py or app factory. Everywhere else, code depends on abstractions.