Clean Architecture
On this page
- At a glance
- The idea in brief
- A. The goal: cheap change
- B. The Dependency Rule
- C. The circles
- D. Crossing a boundary: ports and adapters
- Why I chose it
- My conventions
- How I check a module against it
- Where I do not use it
- A. Web interfaces built with React or Vue
- B. Single-file tools
- C. Fast first versions of a Rust service
- D. Framework-locked projects with an existing team
- E. Boundaries that do not pay for themselves
- References
- Terms
Clean Architecture is a way of structuring software so that the rules a program exists for never depend on the tools that carry them out. The idea and its vocabulary come from Robert C. Martin, in Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017).
It is one of the architectures I use, not the only one. I reach for it when I build a service or an application whose rules should outlive its database, framework, and storage. This page describes why I chose it, the conventions I follow wherever I use it, how I check a module against it, and the places where I deliberately use something else.
At a glance
| Origin | Robert C. Martin, Clean Architecture (2017) |
|---|---|
| I use it for | Backend services and applications whose rules should outlive their tools, including Flutter apps |
| I do not use it for | Web interfaces built with React or Vue, single-file tools, and framework-locked projects such as Laravel |
| Languages | Rust, Python, Dart (Flutter), Node.js, C |
The idea in brief
A. The goal: cheap change
Martin defines the goal of software architecture as minimising the human effort needed to build and maintain a system. A good architecture makes a change cost roughly what the change is worth; a bad one turns a small change into weeks of work because it ripples through code that should never have been involved.
His main tool is the distinction between policy and detail. Policy is what the system is for: the business rules, which would stay the same whether the data sat in PostgreSQL or in plain files. Details are the mechanisms that carry the policy out: the database, the web framework, the HTTP protocol, the storage client. Details are necessary, but they are also the part most likely to change.
B. The Dependency Rule
Everything rests on one rule: source code dependencies point inward only, from details toward policy. Code in an inner layer never mentions a name declared in an outer one. The inner layers do not know which database exists, which framework serves them, or how their results will be displayed. Replacing a tool therefore touches only the outer layers, and the one direction that makes software expensive — a rule changing because a tool changed — cannot happen.
C. The circles
Martin draws the architecture as concentric circles with policy at the centre:
- Entities hold the core rules of the domain, the ones that would stay true even if the application around them were rewritten.
- Use cases hold application-specific rules: what happens, and in which order, when someone asks the system to do something.
- Interface adapters convert between the form the use cases prefer and the form the outside world needs — requests, responses, rows, rendered pages.
- Frameworks and drivers are the tools themselves: the database, the web server, the storage client, the message broker.
He is explicit that four is not a law. What must hold is the direction of the arrows, not the number of circles.
D. Crossing a boundary: ports and adapters
A use case often has to reach something outside, such as saving a record. It cannot call the database directly without breaking the rule, so it declares an interface describing what it needs, and an outer layer provides the implementation. The interface belongs to the inner layer; the implementation lives outside and points inward. I call these interfaces ports and their implementations adapters.
Why I chose it
Every project of mine has the same map. This is the reason I value most. Whether a project is written in Rust, Python, Dart, Node.js, or C, the same four folders sit at the top of its source tree, with the same meaning. When I open a project I have not touched for months, or one in a different language from the one I worked in yesterday, I already know where the use cases live, where the database code lives, and where a request enters. Switching languages costs me syntax, not orientation. The same map helps anyone who reads the code after me, including the AI agents I let search a repository: a use case is found by its name, and its folder already says it contains no SQL and no HTTP.
The tools change, and the rules do not have to. Over the years I replace databases, logging pipelines, storage servers, and web frameworks. Because use cases only know ports, swapping the tool behind a port means writing a new adapter and changing the line in the main component that chooses it. The rules themselves stay untouched, which is the whole point of keeping them apart.
The rules can be tested without the tools. A use case that depends on ports can be handed an in-memory adapter in a test. That makes testing every use case cheap — no database to start, no storage server to reach — so I test the logic that matters instead of only the logic that is easy to reach.
The book gives me a checklist, not just a picture. Martin's chapters are concrete enough to turn into questions I can ask of a finished module. The next sections are the result: the conventions I apply, and the questions I check them against.
My conventions
These are the conventions a module of mine follows when its page says it uses Clean Architecture. The folder names are the same in every language:
| Circle | Folder | What lives there |
|---|---|---|
| Entities | domain/ | entities, value objects, domain errors |
| Use cases | application/ | use cases, the ports they need, input types, validation |
| Interface adapters | interface/ | request handlers, presenters, error-to-response translation |
| Frameworks and drivers | infrastructure/ | adapters for the database, storage, telemetry, configuration |
Two rules keep the map honest. shared/ only holds helpers that belong to no circle, such as encoding or time utilities, and it must not become a place to hide business logic. And the entry point is the only file that knows which adapter stands behind each port; Martin calls it the main component, the dirtiest part of the system, kept small on purpose.
A port is declared by the use case that needs it, and says nothing about how the need is met:
// application/ports: what the use case needs
// infrastructure/persistence: one way to provide it
The same shape works in every language I use; only the word for "a capability without an implementation" changes:
| Language | Port | Adapter for tests |
|---|---|---|
| Rust | trait | struct implementing the trait in memory |
| Python | typing.Protocol or an abstract base class | a small class holding a list or dict |
| Dart (Flutter) | abstract class | a class implementing it in memory |
| TypeScript (Node.js) | interface | an object literal satisfying the interface |
| C | struct of function pointers | a struct filled with test functions |
One variation is allowed. In a Flutter app, the outer layer that faces the user is a set of screens rather than request handlers, so that folder is named presentation/ instead of interface/. Its place in the map is the same.
How I check a module against it
I keep a list of questions that I ask a module chapter by chapter against Martin's book. It is my own review routine, not a rule from the book, and it is how I decide whether a module's structure is finished:
| Area | Question I ask | Chapters |
|---|---|---|
| Design principles | Does each part follow SRP, OCP, LSP, ISP, and DIP? | 7–11 |
| Components | Are cohesion and coupling balanced between ease of change and reuse? | 13–14 |
| Boundaries | Are the lines drawn where the system can be decoupled, with policy and level kept apart? | 16–19 |
| Business rules | Do the rules stand on their own, and does the structure announce what the system does rather than which framework it uses? | 20–21 |
| The core | Does every dependency point inward? | 22 |
| Edges | Are presenters humble, are partial boundaries deliberate, and is the main component the only place that wires things? | 23–26 |
| Services and tests | Do services and tests respect the same boundaries as the code? | 27–28 |
| Details | Are the database, the web, and the framework treated as details? | 30–32 |
Where I do not use it
Clean Architecture has a real cost: more files, more interfaces, and one more step between a request and the code that serves it. In each of the cases below, that cost buys me nothing, so I use a different architecture. Where that architecture has its own page, a card below links to it.
A. Web interfaces built with React or Vue
I do not use it for web front ends in React or Vue, and I consider it an anti-pattern there. These frameworks already define how an interface is structured: components, reactive state, routing. The business rules of my systems live in the backend services behind the interface, not in the browser. Wrapping a Vue or React app in domain and use case layers would duplicate what the framework already provides and push its reactivity through abstractions it was designed not to need.
Flutter is the exception, and I do use Clean Architecture there. My Flutter apps carry real application logic on the device — capturing audio and video, holding sessions, talking to several services at once — and that logic benefits from the same ports and adapters as a backend. Its outer layer is simply called presentation/.
For Vue interfaces I use this architecture instead:
B. Single-file tools
A tool that fits in one file gets no layers at all. Splitting a hundred-line script into four folders would make the structure larger than the program, and would turn something I can read from top to bottom into something I have to navigate. For these tools, the clearest architecture is a single file with a clear order of steps.
The shape I give those tools:
C. Fast first versions of a Rust service
When a Rust service has to work quickly, or when I can only work with a local AI agent whose context window is small, I start with a flat structure instead: one folder per kind of code and one file per operation. It gives up the ports, so the business logic knows its database, but it is quick to build and small enough to keep in view. When the module grows and has to last, I refactor it into Clean Architecture, and the flat folders map onto the circles directly.
The flat structure I start from:
D. Framework-locked projects with an existing team
When a project is built on a framework that already prescribes its own architecture, such as Laravel, and the team working on it knows that framework rather than Clean Architecture, I follow the framework. Laravel has its own conventions for controllers, models, and services, and a team can work productively inside them. Forcing Clean Architecture on top would split the codebase into two styles, slow everyone down, and fight the framework at every step. In that setting, consistency with the framework is worth more than the benefits of the circles.
E. Boundaries that do not pay for themselves
Even where I do use it, I do not draw every boundary the book describes. Martin devotes a chapter to partial boundaries, because a full boundary everywhere is rarely worth its price. When two use cases differ only by a parameter, I keep one use case rather than two nearly identical copies. The Dependency Rule stays intact; the number of files does not grow for the sake of symmetry. The test I apply is the book's own goal: if a boundary makes the next change cheaper, it stays; if it only adds indirection, it goes.
References
Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design, Prentice Hall, 2017 — in particular the chapters on the SOLID principles (7–11), boundaries (17–18), screaming architecture (21), the Clean Architecture itself (22), partial boundaries (24), the main component (26), and the database, the web, and frameworks as details (30–32).
Terms
- Policy
- The rules a system exists to apply, independent of the tools used to apply them.
- Detail
- A mechanism that carries policy out — a database, a web framework, a storage client — and that can be replaced without changing the policy.
- Dependency Rule
- Source code dependencies point inward only, from details toward policy.
- Use case
- An application-specific rule: what happens, and in which order, when someone asks the system to do something.
- Port
- An interface declared by an inner layer, describing a capability it needs from the outside.
- Adapter
- An implementation of a port in an outer layer, for example a database repository or an in-memory version used in tests.
- Main component
- The entry point of a program and the only place that knows which adapter stands behind each port.
- Partial boundary
- A boundary drawn in the source layout but not enforced in full, used where a complete boundary would cost more than it saves.