Clean Architecture
Robert C. Martin's central argument: the architecture of a system should make the business rules the most important thing — and make frameworks, databases, and UI irrelevant details.
A good architecture maximizes the number of decisions not yet made. The longer you can defer committing to a database, a framework, or a delivery mechanism, the more options you keep open.
---
Programming Paradigms
Martin frames architecture on top of three paradigms, each imposing a discipline by removing a capability:
| Paradigm | Removes | Gives us |
|---|---|---|
| Structured | Unrestricted goto | Provable, testable control flow |
| Object-Oriented | Unrestricted function pointers | Safe polymorphism via dependency inversion |
| Functional | Unrestricted assignment (mutable state) | Predictable, side-effect-free code |
---
The Dependency Rule
The most important rule in Clean Architecture:
> Source code dependencies must point only inward — toward higher-level policies.
The inner layers know nothing about the outer layers. Names of things in the outer layers must not appear in the inner layers.
┌───────────────────────────────────────────────┐
│ Frameworks & Drivers (Web, DB, UI, Devices) │
│ ┌─────────────────────────────────────────┐ │
│ │ Interface Adapters (Controllers, │ │
│ │ Presenters, Gateways) │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ Use Cases (Application Rules) │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ Entities (Enterprise Rules) │ │ │ │
│ │ │ └─────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
All arrows point inward →
---
The Four Layers
Entities
Enterprise-wide business rules that would exist even if there were no software.
class Order:
def __init__(self, items: list[OrderItem]):
self.items = items
def total(self) -> Money:
return sum(item.price for item in self.items)
def apply_discount(self, rate: float) -> None:
if rate > 0.5:
raise ValueError("Discount cannot exceed 50%")
for item in self.items:
item.apply_discount(rate)
Entities have no knowledge of databases, HTTP, or frameworks. They are pure business objects.
Use Cases
Application-specific business rules. Orchestrate the flow of data to and from entities to achieve a goal.
class PlaceOrderUseCase:
def __init__(
self,
order_repo: OrderRepository, # interface, not implementation
payment_gateway: PaymentGateway,
notifier: Notifier,
):
self.order_repo = order_repo
self.payment_gateway = payment_gateway
self.notifier = notifier
def execute(self, request: PlaceOrderRequest) -> PlaceOrderResponse:
order = Order(items=request.items)
order.apply_discount(request.discount_rate)
self.payment_gateway.charge(order.total())
self.order_repo.save(order)
self.notifier.send_confirmation(request.customer_email)
return PlaceOrderResponse(order_id=order.id)
Use cases depend only on interfaces (ports). They do not import Flask, SQLAlchemy, or Stripe.
Interface Adapters
Convert data between the format used by use cases/entities and the format used by external agencies (HTTP, database, UI).
# Controller — converts HTTP request to use case input
class PlaceOrderController:
def __init__(self, use_case: PlaceOrderUseCase):
self.use_case = use_case
def handle(self, http_request: HttpRequest) -> HttpResponse:
request = PlaceOrderRequest(
items=http_request.json["items"],
discount_rate=http_request.json.get("discount", 0),
customer_email=http_request.json["email"],
)
response = self.use_case.execute(request)
return HttpResponse(status=201, body={"order_id": response.order_id})
# Gateway — converts use case calls to database operations
class SqlOrderRepository(OrderRepository):
def save(self, order: Order) -> None:
record = OrderRecord(id=order.id, total=float(order.total()))
db.session.add(record)
db.session.commit()
Frameworks & Drivers
The outermost layer: web frameworks, ORMs, databases, UI, external APIs. These are details — they plug into the system but do not shape it.
# main.py — composition root: wire everything together
from flask import Flask
from app.use_cases import PlaceOrderUseCase
from app.adapters import PlaceOrderController, SqlOrderRepository
from app.gateways import StripePaymentGateway, EmailNotifier
app = Flask(__name__)
use_case = PlaceOrderUseCase(
order_repo=SqlOrderRepository(),
payment_gateway=StripePaymentGateway(),
notifier=EmailNotifier(),
)
controller = PlaceOrderController(use_case)
@app.post("/orders")
def place_order():
return controller.handle(request)
---
Screaming Architecture
The top-level structure of a codebase should scream what the system does, not what framework it uses.
❌ Framework-screaming (what tool, not what domain):
src/
controllers/
models/
views/
migrations/
✅ Domain-screaming (what the system does):
src/
orders/
billing/
inventory/
notifications/
If someone opens the project and sees "FastAPI" or "Django" before they see "orders" or "billing", the architecture is framework-centric. The business domain should be front and center.
---
SOLID Principles in Architecture
Martin uses SOLID as the bridge between code-level design and component-level architecture.
Single Responsibility Principle
> A module should be responsible to one, and only one, actor (group of stakeholders).
# ❌ Serves two actors: Finance (calculates pay) and HR (reports hours)
class Employee:
def calculate_pay(self): ... # Finance owns this
def report_hours(self): ... # HR owns this
def save(self): ... # DBA owns this
# ✅ Separated by actor
class PayCalculator:
def calculate(self, employee): ...
class HourReporter:
def report(self, employee): ...
class EmployeeRepository:
def save(self, employee): ...
Open/Closed Principle
> Behavior should be extensible without modification. The architecture controls what is easy to extend.
Achieved by organizing components so that high-value, stable components are protected from change — new behavior is added by extending, not editing.
Liskov Substitution Principle
> Subtypes must be substitutable for their base types without breaking the system.
In architecture, this applies to services: if a component expects an OrderRepository, any implementation of OrderRepository must be substitutable without changing the caller.
Interface Segregation Principle
> Don't depend on things you don't use.
# ❌ Fat interface forces unnecessary dependencies
class OrderRepository:
def save(self, order): ...
def delete(self, order_id): ...
def generate_report(self): ... # unrelated to most callers
# ✅ Focused interfaces
class OrderWriter:
def save(self, order): ...
class OrderDeleter:
def delete(self, order_id): ...
class OrderReporter:
def generate_report(self): ...
Dependency Inversion Principle
The architectural cornerstone. High-level policy must not depend on low-level details.
# ❌ Use case directly depends on a concrete implementation
class PlaceOrderUseCase:
def execute(self, request):
db = PostgreSQLDatabase() # high-level depends on detail
db.save(...)
# ✅ Use case depends on an abstraction (interface/port)
class PlaceOrderUseCase:
def __init__(self, order_repo: OrderRepository): # interface
self.order_repo = order_repo
def execute(self, request):
self.order_repo.save(...) # calls interface, not impl
The dependency arrow is inverted: PostgreSQLDatabase depends on OrderRepository (implements it), not the other way around.
---
Component Cohesion Principles
These three principles decide what belongs in a component together.
Reuse/Release Equivalence Principle (REP)
> Things that are released together should be grouped together.
A component is a unit of release. All classes inside it should be cohesive enough that users would want them together in a dependency.
Common Closure Principle (CCP)
> Group things that change for the same reason at the same time.
This is SRP applied to components. If a change touches 10 files in 5 components, the architecture is working against the team. If it touches 10 files in 1 component, the architecture absorbs change cleanly.
Common Reuse Principle (CRP)
> Don't force users to depend on things they don't use.
When a component is split, things that are not used together should not be packaged together — every unnecessary dependency is a recompile and redeploy trigger.
The Tension Triangle:
REP
(group for reuse)
/ \
/ \
CCP --------- CRP
(group by change) (split by use)
Too far toward CCP+REP → too many unrelated things together
Too far toward CCP+CRP → too many components, painful assembly
Too far toward REP+CRP → too many small packages, hard to change cohesively
---
Component Coupling Principles
These three principles control dependencies between components.
Acyclic Dependencies Principle (ADP)
> There must be no cycles in the component dependency graph.
A cycle means a group of components that must always be released together — defeating the purpose of separate components.
❌ Cycle:
OrderService → UserService → BillingService → OrderService
✅ Break the cycle with a new component or by inverting a dependency:
OrderService → UserService → BillingService
OrderService → BillingInterfaces ← BillingService
Stable Dependencies Principle (SDP)
> Depend in the direction of stability.
A stable component is one that is hard to change (many things depend on it). An unstable component changes often.
Instability = efferent coupling / (afferent + efferent coupling)
0 = maximally stable (many things depend on it, depends on nothing)
1 = maximally unstable (depends on many, nothing depends on it)
Rule: a component's instability must be ≥ the instability of components it depends on
Do not make a stable component depend on an unstable one — a change in the unstable component forces changes in the stable one.
Stable Abstractions Principle (SAP)
> A component should be as abstract as it is stable.
Stable components that many things depend on should be abstract (interfaces, abstract classes) so they can be extended without modification.
Abstract + Stable = ideal (interfaces that many depend on)
Concrete + Unstable = ideal (leaf implementations that change often)
Concrete + Stable = "Zone of Pain" (hard to change, concrete — a problem)
Abstract + Unstable = "Zone of Uselessness" (abstract, but nobody depends on it)
---
The Humble Object Pattern
Separates testable logic from hard-to-test behavior by putting the hard part behind an interface.
Hard to test: database writes, HTTP responses, UI rendering
Easy to test: formatting, calculations, transformations
Split:
Humble Object → the hard part (minimal logic, just calls the hard thing)
Testable Object → all the logic, takes and returns simple data structures
Applied to a presenter:
# Testable: pure logic, no UI dependency
class OrderPresenter:
def format(self, order: Order) -> OrderViewModel:
return OrderViewModel(
id=str(order.id),
total=f"Rp {order.total():,.0f}",
status=order.status.display_name,
)
# Humble: just passes the view model to the actual UI renderer
class OrderView:
def render(self, view_model: OrderViewModel) -> HttpResponse:
return render_template("order.html", data=view_model)
The OrderPresenter is fully testable without a web server or database. The OrderView is so simple it barely needs testing.
---
The Database Is a Detail
The database is not the center of the application. It is a plugin.
❌ Database-centric:
Business rules call ORM models directly
Entities are SQLAlchemy/Django models
Schema changes ripple into business logic
✅ Database as a detail:
Business rules depend on repository interfaces
SQLAlchemy is only mentioned in the outermost layer
Schema changes stay inside the adapter layer
Martin's rule: if you can swap your database from PostgreSQL to MongoDB (or an in-memory store for tests) by only changing one adapter class, the database is properly treated as a detail.
---
The Web Is a Detail
HTTP is a delivery mechanism, not the application.
❌ Web-centric:
Use cases import Flask/Django
Business rules depend on HttpRequest objects
Testing requires starting a web server
✅ Web as a detail:
Use cases accept plain Python objects (request DTOs)
Controllers convert HttpRequest → DTO → use case
Use cases are tested without any HTTP infrastructure
The same use case should be callable from an HTTP endpoint, a CLI command, a background job, or a test — without modification.
---
Folder Structure Example
src/
domain/ ← Entities (innermost)
order.py
payment.py
use_cases/ ← Use Cases
place_order.py
cancel_order.py
ports/ ← Interfaces (abstract repos, gateways)
order_repository.py
payment_gateway.py
adapters/ ← Interface Adapters
http/
order_controller.py
persistence/
sql_order_repository.py
messaging/
kafka_event_publisher.py
infrastructure/ ← Frameworks & Drivers (outermost)
database.py
flask_app.py
main.py ← Composition root
Dependency direction: infrastructure → adapters → use_cases → domain. Nothing flows outward.
---
Key Takeaways
| Principle | Rule |
|---|---|
| Dependency Rule | Source code dependencies point inward only |
| Entities | Pure business rules — no framework imports |
| Use Cases | Application logic — depend on interfaces, not implementations |
| Interface Adapters | Convert between domain types and external formats |
| Screaming Architecture | Top-level structure names the domain, not the framework |
| Database is a detail | Swap persistence by changing one adapter |
| Web is a detail | Use cases are callable without HTTP |
| Humble Object | Separate testable logic from hard-to-test infrastructure |
| ADP | No cycles in component dependencies |
| SDP + SAP | Stable components must be abstract |