Skip to content
Architecture Programming

Clean Architecture

By Romi Fadli, S.Kom. 12 min read Software Design
On this page
Four concentric circles with arrows coming in from a database, a browser window, and a message queue, all pointing toward the centre.

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

OriginRobert C. Martin, Clean Architecture (2017)
I use it forBackend services and applications whose rules should outlive their tools, including Flutter apps
I do not use it forWeb interfaces built with React or Vue, single-file tools, and framework-locked projects such as Laravel
LanguagesRust, Python, Dart (Flutter), Node.js, C
Where Clean Architecture sits among the architectures I use.

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:

bash
src/
├── domain/          # entities, value objects, domain errors — imports nothing outside
├── application/     # use_cases/, ports/, dto/, validation/
├── infrastructure/  # adapters: persistence/, config/, telemetry/, bootstrap/
├── interface/       # how the outside calls in: http/, presenters/
├── shared/          # small helpers with no business meaning
└── main             # the main component: picks an adapter for every port
CircleFolderWhat lives there
Entitiesdomain/entities, value objects, domain errors
Use casesapplication/use cases, the ports they need, input types, validation
Interface adaptersinterface/request handlers, presenters, error-to-response translation
Frameworks and driversinfrastructure/adapters for the database, storage, telemetry, configuration
How Martin's circles map onto my folders.

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:

rust
// application/ports: what the use case needs
pub trait RecordStore {
    fn save(&self, record: &Record) -> Result<(), StoreError>;
}

// infrastructure/persistence: one way to provide it
pub struct PostgresRecordStore { /* connection pool */ }
impl RecordStore for PostgresRecordStore { /* … */ }

The same shape works in every language I use; only the word for "a capability without an implementation" changes:

LanguagePortAdapter for tests
Rusttraitstruct implementing the trait in memory
Pythontyping.Protocol or an abstract base classa small class holding a list or dict
Dart (Flutter)abstract classa class implementing it in memory
TypeScript (Node.js)interfacean object literal satisfying the interface
Cstruct of function pointersa struct filled with test functions
How a port and its test adapter are written in each language.

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:

AreaQuestion I askChapters
Design principlesDoes each part follow SRP, OCP, LSP, ISP, and DIP?7–11
ComponentsAre cohesion and coupling balanced between ease of change and reuse?13–14
BoundariesAre the lines drawn where the system can be decoupled, with policy and level kept apart?16–19
Business rulesDo the rules stand on their own, and does the structure announce what the system does rather than which framework it uses?20–21
The coreDoes every dependency point inward?22
EdgesAre presenters humble, are partial boundaries deliberate, and is the main component the only place that wires things?23–26
Services and testsDo services and tests respect the same boundaries as the code?27–28
DetailsAre the database, the web, and the framework treated as details?30–32
The questions I check a module against, grouped by the chapters they come from.

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.

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

Three identical empty application nodes stacked as copies, connected to a database, a cache, object storage, a pool of queue workers, and a telemetry collector that hold all the state

How I run Laravel as the core of a team-built platform: nodes that hold no state, slow work handed to queued jobs and specialised services, files kept off the server, one internal token for every login, and every request, job, and call observable.

13 min read Read more →
One endpoint procedure as a column of ordered steps, a request entering at the top and the answer leaving at the bottom, each step calling its own concern module beside it

Each endpoint is one procedure that carries a request from start to finish, with its concerns in modules beside it — Fowler's Transaction Script as I shape small one-job Python services, bundled into one binary, until their rules outgrow it.

8 min read Read more →
A four-level tree of Indonesian regions, with one chosen path coloured level by level, feeding a form of four cascading dropdowns in the same colours

A Rust service that serves Indonesia's administrative regions — 38 provinces down to 83,768 villages, keyed by the official Ministry of Home Affairs codes — as four cascading lookups with search, so every address form in every project gets the same answer.

10 min read Read more →
A features folder holding three feature folders — orders, users, reports — with a blue line flowing one way into stores, services and an HTTP client, and a purple line to a design system pinned at version 1.0.0

Feature-Based Frontend Architecture is one of the architectures I use: the shape I give Vue web interfaces — one folder per feature, shared code in a few global folders by kind, data flowing one way through stores and services to one HTTP client, and my own design system pinned by version.

7 min read Read more →
Four flat layers stacked on the left — routes, controllers, services, repositories — joined by short lines, with an orange line labelled later: refactor leading to the concentric circles of Clean Architecture on the right

One folder per kind of code at a single level, a short path from route to repository, one file per operation — the shape I give a Rust service that must work fast or fit a local AI agent's context, before it grows into Clean Architecture.

7 min read Read more →
A single file card holding six sections in a fixed order — Config, Helpers, Reporter, Steps, Summary, Entrypoint — with arguments, a config file and a prompt flowing in from the left and exit codes 0, 1, 2 and 130 flowing out to the right

The shape I give small tools: one file, one job, sections in a fixed order, an exit-code contract at the top, and three ways in for input — plus its Bash variant for generators, and when a tool outgrows it.

8 min read Read more →
A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

A small Rust service between applications and S3-compatible storage: it hands out presigned links so browsers talk to storage directly, gives each file a lifecycle, copies between buckets on the server, and keeps storage credentials out of every application.

12 min read Read more →

Related Articles

A four-level tree of Indonesian regions, with one chosen path coloured level by level, feeding a form of four cascading dropdowns in the same colours

A Rust service that serves Indonesia's administrative regions — 38 provinces down to 83,768 villages, keyed by the official Ministry of Home Affairs codes — as four cascading lookups with search, so every address form in every project gets the same answer.

10 min read Read more →
A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

A small Rust service between applications and S3-compatible storage: it hands out presigned links so browsers talk to storage directly, gives each file a lifecycle, copies between buckets on the server, and keeps storage credentials out of every application.

12 min read Read more →