What clean architecture is
Clean architecture is a way to structure an application so its business rules don't depend on frameworks, databases or user interfaces. Code is arranged in layers around the domain, and one rule holds it together: source code dependencies point only inward, toward the business rules.
Robert C. Martin described it in a 2012 blog post and in his 2017 book of the same name. It builds on earlier designs with the same goal, including Alistair Cockburn's hexagonal architecture, also called ports and adapters, and Jeffrey Palermo's onion architecture.
The payoff is that you can test business logic without a server or a database. You can also replace the web framework or the storage with limited changes to the core.
The dependency rule
Inner layers never name anything in an outer layer. Domain code has no import of the web framework, the ORM, an HTTP library or a vendor SDK. Data that crosses a boundary travels as plain objects that the inner layer defines.
Control still flows outward at runtime. A use case calls a repository to save an order, and the repository talks to the database. The use case calls an interface it declares itself, and the database code implements that interface. That is the dependency inversion principle applied to a whole application.
Martin describes four layers, from the center out:
- Entities hold the core business objects and rules, such as how an invoice total is calculated.
- Use cases are application actions, such as issuing an invoice or cancelling a subscription. They load entities, apply rules and save the result.
- Interface adapters, such as controllers, presenters and repository implementations, convert data between the core and the outside world.
- Frameworks and drivers are the web framework, the database, the message broker and third-party SDKs.
Use cases and the domain at the center
The domain holds rules that are true regardless of how the app is delivered. An invoice can't be issued with no line items, a discount can't exceed the subtotal, and a payment is overdue after its due date. These rules are plain functions and types with no I/O.
A use case coordinates one application action. IssueInvoice validates the input, loads the customer through a port, applies domain rules, saves the invoice and asks a notifier to send it. It returns a result or a typed error and knows nothing about HTTP status codes or response formats.
Because neither layer touches the network or the disk, their tests run in milliseconds with in-memory fakes. A route handler shrinks to a thin step: authenticate, validate input, call the use case and map the result to a response.
Ports and adapters
A port is an interface the core declares for something it needs or offers. An adapter connects that port to a real technology. A PaymentGateway port with a charge method might have a Stripe adapter in production and a fake adapter in tests.
Hexagonal architecture names two kinds of adapter. Driving adapters call into the application: an HTTP controller, a CLI command or a queue consumer. Driven adapters are called by it: a database repository, an email sender or a payment client.
Define ports in the core's language, not a vendor's. A charge method that takes an amount and a customer ID survives a change of payment provider. A port that mirrors one provider's SDK parameters ties the core to that provider anyway.
How it maps to folders in a web app
Folder names vary by team, but a TypeScript web app often ends up with a layout like the one below. Each folder may import only from folders closer to the center.
Folder names alone enforce nothing. Add a lint rule, such as ESLint's no-restricted-imports or a dependency-cruiser configuration, that fails the build when domain code imports from infrastructure or a framework package.
Once the app grows, group by feature inside each layer, such as domain/billing and domain/accounts, so related code stays together.
src/
domain/ entities, value objects, business rules (no I/O)
application/ use cases and the ports they need
infrastructure/ adapters: database repositories, email, payment clients
app/ routes, controllers and UI (framework code)
main.ts composition root: builds adapters, passes them to use casesWhen clean architecture is overkill
The layers cost something: more files, mapping code between layers and indirection a new reader has to trace. For a CRUD app whose logic is mostly 'save this form and show that list', that cost buys little. Prototypes and short-lived tools rarely repay it either.
Swapping the database is a weak reason on its own. It rarely happens, and a port that hides every database feature can stop you from using the features you chose that database for.
A lighter starting point works for most apps. Move business rules out of route handlers and UI components into plain functions. Add a port only when you need a second implementation or a fast test. Grow toward the full structure as the domain logic grows.