Source: Robert C. Martin, "The Clean Architecture" (2012). I wrote this from my knowledge of the article and did not fetch the page in this session. The code is my own illustration in Python and I did not run it.
Clean Architecture divides software into layers. Each layer has one concern. The article lists five properties of a system that obeys the architecture:
- Independent of frameworks. The framework is a tool. The system does not conform to the constraints of the framework.
- Testable. You can test the business rules without the UI, the database, or the web server.
- Independent of the UI. You can replace a web UI with a console UI, and the business rules do not change.
- Independent of the database. You can replace one database with a different one, and the business rules do not change.
- Independent of external agencies. The business rules know nothing about the outside world.
The article shows the layers as concentric circles. The list goes from the innermost circle to the outermost circle.
| Layer | Contents | Reason to change |
|---|---|---|
| Entities | Business rules that apply to the full enterprise | The business changes |
| Use cases | Business rules that apply to one application | The operation of the application changes |
| Interface adapters | Controllers, presenters, gateways | A data format changes |
| Frameworks and drivers | Web framework, database, devices | A tool changes |
The number four is not a rule. A system can have more circles. The Dependency Rule always applies.
Source code dependencies point only inward.
Code in an inner circle does not name anything that an outer circle declares. This includes functions, classes, variables, and data formats. An entity does not import a use case. A use case does not import a controller, an ORM model, or a framework.
The entity contains a business rule. It imports no other layer.
# domain/order.py
from dataclasses import dataclass
@dataclass
class Order:
order_id: str
total_cents: int
paid: bool = False
def pay(self, amount_cents: int) -> None:
if self.paid:
raise ValueError("The order is already paid.")
if amount_cents != self.total_cents:
raise ValueError("The amount must equal the order total.")
self.paid = TrueThe use case controls the flow of data to and from the entities. It also declares the interfaces that it needs. The article calls these interfaces ports.
# use_cases/pay_order.py
from dataclasses import dataclass
from typing import Protocol
from domain.order import Order
class OrderRepository(Protocol):
def get(self, order_id: str) -> Order: ...
def save(self, order: Order) -> None: ...
class PayOrderOutputPort(Protocol):
def present(self, response: "PayOrderResponse") -> None: ...
@dataclass(frozen=True)
class PayOrderRequest:
order_id: str
amount_cents: int
@dataclass(frozen=True)
class PayOrderResponse:
order_id: str
paid: bool
class PayOrder:
def __init__(self, orders: OrderRepository, output: PayOrderOutputPort) -> None:
self._orders = orders
self._output = output
def execute(self, request: PayOrderRequest) -> None:
order = self._orders.get(request.order_id)
order.pay(request.amount_cents)
self._orders.save(order)
self._output.present(PayOrderResponse(order.order_id, order.paid))The import list contains only domain. The use case does not know SQL, HTTP, or JSON.
An adapter converts data between the format of the use cases and the format of an external tool. All SQL stays in this layer.
# adapters/sql_order_repository.py
import sqlite3
from domain.order import Order
class SqlOrderRepository:
def __init__(self, connection: sqlite3.Connection) -> None:
self._connection = connection
def get(self, order_id: str) -> Order:
row = self._connection.execute(
"SELECT id, total_cents, paid FROM orders WHERE id = ?", (order_id,)
).fetchone()
return Order(order_id=row[0], total_cents=row[1], paid=bool(row[2]))
def save(self, order: Order) -> None:
self._connection.execute(
"UPDATE orders SET paid = ? WHERE id = ?", (order.paid, order.order_id)
)# adapters/pay_order_web.py
from use_cases.pay_order import PayOrder, PayOrderRequest, PayOrderResponse
class JsonPayOrderPresenter:
def __init__(self) -> None:
self.view_model: dict = {}
def present(self, response: PayOrderResponse) -> None:
status = "paid" if response.paid else "unpaid"
self.view_model = {"orderId": response.order_id, "status": status}
class PayOrderController:
def __init__(self, use_case: PayOrder) -> None:
self._use_case = use_case
def handle(self, order_id: str, body: dict) -> None:
self._use_case.execute(PayOrderRequest(order_id, body["amountCents"]))This layer contains the framework and the code that assembles the objects. It is the only layer that knows all the other layers.
# main.py
import sqlite3
from fastapi import FastAPI
from adapters.pay_order_web import JsonPayOrderPresenter, PayOrderController
from adapters.sql_order_repository import SqlOrderRepository
from use_cases.pay_order import PayOrder
app = FastAPI()
@app.post("/orders/{order_id}/pay")
def pay_order(order_id: str, body: dict) -> dict:
connection = sqlite3.connect("shop.db")
presenter = JsonPayOrderPresenter()
use_case = PayOrder(SqlOrderRepository(connection), presenter)
PayOrderController(use_case).handle(order_id, body)
connection.commit()
return presenter.view_modelThe flow of control and the direction of the dependencies are different.
- The controller calls the use case. Control goes inward, and the dependency points inward.
- The use case calls the presenter. Control goes outward.
- The use case cannot import the presenter, because the Dependency Rule prohibits that import.
- Thus the use case declares
PayOrderOutputPort, and the presenter implements it. - The source code dependency now points inward, against the flow of control.
This is the Dependency Inversion Principle. The repository uses the same method: the use case declares OrderRepository, and the adapter implements it.
Only simple data structures cross a boundary. PayOrderRequest and PayOrderResponse are examples.
Do not pass a database row or an ORM object inward. If you do, the inner circle must know the format of the outer circle. That breaks the Dependency Rule.
A test of the business rules needs no database and no web server. The test supplies simple objects that satisfy the ports.
# tests/test_pay_order.py
from domain.order import Order
from use_cases.pay_order import PayOrder, PayOrderRequest
class InMemoryOrders:
def __init__(self, order: Order) -> None:
self._order = order
def get(self, order_id: str) -> Order:
return self._order
def save(self, order: Order) -> None:
self._order = order
class RecordingOutput:
def present(self, response) -> None:
self.response = response
def test_pay_order_marks_the_order_as_paid() -> None:
output = RecordingOutput()
use_case = PayOrder(InMemoryOrders(Order("o-1", 5000)), output)
use_case.execute(PayOrderRequest("o-1", 5000))
assert output.response.paid is True- The inner circles contain policy. The outer circles contain mechanisms.
- Dependencies point inward only.
- An inner circle declares an interface when it must call an outer circle.
- Simple data structures cross each boundary.
- You can replace the database, the framework, or the UI, and the business rules do not change.