Most engineers have seen the clean architecture picture: four colored rings with arrows pointing at the middle. Far fewer can say what goes into each ring for a real feature, which way the imports should point, or where the SQL lives.
The gap shows up in code review: a domain folder that imports the ORM, a use case that returns an HTTP status code, five mappers between a form and a table.
This post turns Robert C. Martin's circles into a component diagram for one use case, placing an order: the layers, the dependency rule, code and folders, boundary crossings, clean versus hexagonal and onion, mistakes, when it is overkill, and how to draw it.
The four layers
Martin described the clean architecture in a 2012 blog post, The Clean Architecture, and expanded it in his 2017 book Clean Architecture. He presented it as an integration of earlier ideas, including hexagonal and onion architecture. From the center outward:
| Layer | What it holds | In the example | Changes when |
|---|---|---|---|
| Entities | Enterprise business rules, reusable across applications | Order and its rule that an order needs at least one line |
A business rule changes |
| Use cases | Application business rules, one workflow each | PlaceOrder, its two interfaces, request and response models |
The workflow changes |
| Interface adapters | Controllers, presenters, gateways and all the SQL | OrderController, OrderPresenter, PostgresOrderRepository |
The API or the schema changes |
| Frameworks and drivers | Tools plus glue code: web framework, database, drivers | The HTTP framework, PostgreSQL, pg, main.ts |
You replace a tool |
The circles are schematic; Martin says you may need more than four. The rule connecting them never changes.
The dependency rule
In Martin's words, "source code dependencies can only point inwards." Code in an inner circle never mentions a function, class or variable declared in an outer one, or uses its data formats. For this example:
entitiesimports nothing from the other layers.use-casesimports onlyentities: no web framework, ORM or driver.adaptersimport inward, plus the library they adapt.- Only
main.tssees everything.
The rule covers source code, not runtime calls: at runtime PlaceOrder calls outward, to the presenter and the database. Martin resolves this with the dependency inversion principle: the use case calls an interface it owns, and an outer class implements it. The call goes out; the dependency points in.
One dependency does point outward: the adapter imports the database driver. That is by design. Martin puts all SQL in the adapter layer, and no code inward of it "should know anything at all about the database."
Enforce the rule in CI with the onionArchitecture() check in ArchUnit on the JVM, dependency-cruiser for TypeScript or the layers contract of import-linter for Python.
Worked example: placing an order
A POST /orders request takes this path:
- The web framework routes it to
OrderController. - The controller parses the body into a
PlaceOrderRequest, a plain object owned by the use case layer, creates anOrderPresenterand callsPlaceOrder.execute(). PlaceOrderletsOrder.place()apply the business rules, saves throughOrderRepositoryand passes aPlaceOrderResponsetoPlaceOrderOutput.OrderPresenterimplements that interface and builds a view model: status 201, aLocationheader and a JSON body.- The controller returns the view model, and the framework writes it.
The use case layer, with both interfaces it owns:
// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';
export interface OrderRepository { // implemented by an adapter
nextId(): Promise<string>;
save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput { // implemented by the presenter
present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
customerId: string;
lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };
export class PlaceOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
await this.orders.save(order);
output.present({ orderId: order.id, itemCount: order.itemCount() });
}
}
It imports only the entity. Order.place() throws when an order has no lines or a quantity below one, and the entity file imports nothing. The adapter imports inward, plus the driver:
// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg'; // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async nextId(): Promise<string> { return randomUUID(); }
async save(order: Order): Promise<void> {
await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
[order.id, order.customerId, JSON.stringify(order.lines)]);
}
}
The composition root alone knows every concrete class:
// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));
app.post('/orders', async (req, res) => {
const view = await controller.post(req.body); // the presenter's view model
res.status(view.status).set(view.headers).json(view.body);
});
A unit test of PlaceOrder needs only an in-memory repository and a presenter that records its input: no server, no database.
Folder structure
src/
orders/
entities/
order.ts Order and its rules
use-cases/
place-order.ts PlaceOrder, both interfaces, request and response models
adapters/
http/
order-controller.ts
order-presenter.ts
postgres/
postgres-order-repository.ts
main.ts composition root: builds and wires everything
Group by feature, then by layer. Martin's screaming architecture argument: top-level folders should say what the system does (orders, payments), not which framework it uses. Simon Brown's chapter in the same book separates organization from encapsulation: folders organize code, while access rules or a build check enforce it.
Crossing boundaries
Each boundary crossing carries a type that one layer owns:
| Crossing | What travels | Type owned by | Mechanism |
|---|---|---|---|
| Framework → controller | HTTP request | Framework | Glue code in main.ts |
| Controller → use case | PlaceOrderRequest |
Use cases | Direct call |
| Use case → presenter | PlaceOrderResponse |
Use cases | PlaceOrderOutput interface |
| Use case → database | Order |
Entities | OrderRepository interface |
| Presenter → framework | View model | Adapters | Returned by the controller |
Three habits keep these crossings clean:
- Plain request and response models. Martin is blunt: "We don't want to cheat and pass Entities or Database rows." The presenter gets
PlaceOrderResponse, neverOrder, so renaming an entity field leaves the API alone. Repositories are a common exception: many codebases let them accept entities, which still points inward. Stricter teams map to a persistence record inside the adapter. - An interface at each outward call, owned by the inner layer and named for what the use case needs, not for the technology.
- One composition root. Mark Seemann defines the composition root as "a (preferably) unique location in an application where modules are composed together," close to the entry point. Martin calls
main"the ultimate detail." Everything else receives dependencies through constructors.
Martin's figure also has a Use Case Input Port interface in front of the interactor. With one controller and one implementation, calling the class directly is a common simplification.
Clean vs hexagonal vs onion architecture
| Hexagonal (ports and adapters) | Onion | Clean | |
|---|---|---|---|
| Defining text | Alistair Cockburn, 2005 article | Jeffrey Palermo, 2008 blog series | Robert C. Martin, 2012 blog post, 2017 book |
| Picture | Application in a hexagon, adapters outside | Rings around the domain model | Four concentric circles |
| Structure inside | Not prescribed | Domain model, domain services, application services | Entities, use cases |
| Repository implementation | Secondary (driven) adapter | Infrastructure, outer ring | Interface adapters |
| Core rule | Inside code must not leak outside; talk through ports | All coupling points toward the center | Source code dependencies point inward |
All three put business rules in the middle and make the edges depend on them. They differ in how much structure they prescribe inside: Cockburn's original article separates only inside from outside, Palermo's onion architecture names rings around the domain model, and clean architecture adds presenters and request and response models. The hexagonal architecture diagram walks through a similar order example in ports-and-adapters terms.
Common mistakes
- Framework types in entities. ORM decorators or serialization attributes on
Ordertie the innermost layer to the outermost. Map in the adapter. - A mapper for everything. Six copies of the same fields for a form that saves one row. Map only where the shapes differ.
- Layers without behavior. Getter-and-setter entities, the anemic domain model Martin Fowler described in 2003, and use cases that only call
save(). - Interfaces everywhere. They belong at boundaries, not on every class.
- HTTP in the use case. Status codes belong in the presenter.
When it is overkill
Palermo wrote that onion architecture "is not appropriate for small websites," and Martin's chapter on partial boundaries opens with "Full-fledged architectural boundaries are expensive."
| Situation | A better fit |
|---|---|
| CRUD service with few rules | A thin layered app; keep the few rules out of controllers |
| Prototype or short-lived tool | No layers |
| One volatile dependency, such as a payment provider | One interface and one adapter for it |
| Long-lived product with real rules and several entry points | All four layers |
That chapter also describes cheaper partial boundaries; promote one when a second implementation or a test needs it.
The clean architecture diagram
Diagram contents
- Frameworks and drivers
- Interface adapters
- Use cases
- Entities
- Web framework
- PostgreSQL
- OrderPresenter
- OrderController
- PostgresOrderRepository
- PlaceOrderOutput
- PlaceOrder
- OrderRepository
- Order
- POST /orders
- execute(request)
- Order.place()
- reads view model
- present(response)
- save(order)
- implements
- SQL via pg driver
How to read it:
- Four UML packages, outermost on the left. Moving right means moving inward.
- Solid arrows are calls and imports. Three cross a boundary toward the entities. The other three stay within one layer:
present(response)andsave(order)call the two interfaces the use case layer owns. - Dashed arrows with hollow triangles mean implements. The presenter and the repository point inward at those interfaces while control flows outward.
- The dotted line is SQL through the driver, and it starts in an adapter.
- The notes and the Markdown block say where
main.tslives, what the entity enforces and what each line style means.
Drawing clean architecture diagrams clearly
- Draw one use case, not the rings. Trace a real feature through real class names.
- Put each interface in the layer that owns it.
OrderRepositorysits next toPlaceOrder, not next to its implementation. - Use two arrow styles: solid for calls, dashed with a hollow triangle for implements, as in UML realization. The UML class diagram example covers the notation.
- Give inward one direction on the page. An arrow pointing back is a violation or a runtime call to label.
- Label arrows with the method and the model that crosses.
- Leave out DTO fields, framework internals and the DI container.
For the system around this code-level view, see the C4 model diagram example and the guide to drawing software architecture diagrams.
Build it in Archboard
- Sign in with Google or GitHub and create a diagram.
- Add four Package nodes from the UML pack in a row: Frameworks and drivers, Interface adapters, Use cases and Entities, with blue, green, red and amber accents.
- In Frameworks and drivers, add a Component from the UML pack named Web framework and a Postgres node from the Data pack named PostgreSQL.
- In Interface adapters, add two Component nodes, OrderPresenter and OrderController, and a Class named PostgresOrderRepository.
- In Use cases, add a Class named PlaceOrder between two Interface nodes, PlaceOrderOutput and OrderRepository.
- In Entities, add a Class named Order with its fields and methods.
- Draw six solid labeled edges for the calls, two dashed edges with triangle arrowheads for implements, and a dotted edge from PostgresOrderRepository to PostgreSQL.
- Add a Markdown block from the Core pack with the rule and the line styles, and Text notes from the Core pack for each layer.
A Mermaid flowchart of your layers becomes a first draft with the Mermaid to editable diagram tool. Free allows 15 diagram nodes per diagram, so trace one use case per diagram there.
The example uses 14 diagram nodes, within the Free plan.
FAQ
What are the four layers of clean architecture? Entities (enterprise business rules), use cases (application business rules), interface adapters (controllers, presenters and gateways) and frameworks and drivers (the web framework, the database and other tools). Martin calls the circles schematic: you may need more than four.
What is the dependency rule? Source code dependencies point only inward. Code in an inner layer never names anything declared in an outer layer, so use cases know nothing about controllers, SQL or the web framework.
What is the difference between clean architecture and hexagonal architecture? Both keep business rules in the middle and make adapters depend on them. Hexagonal architecture separates only the inside from the outside, through ports. Clean architecture also splits the inside into entities and use cases and names presenters and request and response models.
Where does the repository interface go in clean architecture? In the use case layer, next to the use case that needs it. The implementation, with the SQL, belongs in the interface adapters layer, and the composition root passes it in at startup.
Is clean architecture overkill for small projects? Often. For CRUD services, prototypes and short-lived tools, the extra interfaces and mappings cost more than they return. Add interfaces for the dependencies you expect to change, and grow the layers when real business rules appear.