Clean Architecture
How layering and the dependency rule keep business logic independent of frameworks, databases, and UI — so the core stays stable, testable, and swappable.
Clean Architecture (and its cousins Hexagonal / Ports & Adapters and Onion Architecture) organizes code into concentric layers with one rule: dependencies point inward, toward the stable business core. Frameworks, databases, and UI become replaceable plugins at the edge. The result is software whose most important code — the business rules — doesn't depend on, and can't be broken by, the volatile details around it.
Code is organized in rings: Entities (core domain) at the center, then Use Cases (application rules), then Interface Adapters (controllers, presenters, repository implementations), then Frameworks & Drivers (web, DB, UI) on the outside. The center is the most stable; the edges are the most volatile.
Source-code dependencies point only inward. Outer layers may depend on inner ones, never the reverse. Entities know nothing about use cases; use cases know nothing about the web or database. This single rule is what makes the architecture 'clean'.
But a use case still needs to save data, which lives in an outer layer. The trick: the use case defines an interface (a 'port') and the outer layer implements it (an 'adapter'). At runtime the concrete adapter is injected — so control flows outward while dependencies still point inward.
The web framework, ORM, database, and UI are plugins at the edge, not the foundation. Because the core depends only on abstractions, you can swap Express for Fastify, Postgres for Mongo, or REST for gRPC without touching a single business rule — and you can test the core with zero infrastructure.
Key Concepts
Source dependencies point only inward, toward higher-level policy. The non-negotiable core of clean architecture.
Enterprise-wide business objects and rules. The innermost, most stable layer — pure domain, no framework.
Application-specific rules that orchestrate entities to fulfill one user action. Depend only on entities.
Interfaces defined by the core (ports) and implemented by outer layers (adapters). Aka Hexagonal Architecture.
Controllers, presenters, and repository implementations that translate between the outside world and use cases.
The single outer place where concrete implementations are wired into the abstractions (the DI container).
1// The dependency rule via ports & adapters.2// Use case depends on an interface it OWNS — not on the database.34// --- core (use cases layer) ---5interface OrderRepository { // a "port" defined inward6 save(order: Order): Promise<void>;7}89class PlaceOrder { // application business rule10 constructor(private repo: OrderRepository) {}11 async execute(cmd: PlaceOrderCommand) {12 const order = Order.create(cmd.items); // entity enforces invariants13 await this.repo.save(order);14 return order.id;15 }16}1718// --- outer (interface adapters layer) ---19class SqlOrderRepository implements OrderRepository { /* Postgres */ }2021// --- composition root (frameworks layer) ---22const useCase = new PlaceOrder(new SqlOrderRepository());23// Swap to MongoOrderRepository and PlaceOrder never changes.
Clean Architecture is the dominant way large .NET and enterprise systems are structured, and it's a frequent interview and design-review topic. Its real payoff is changeability: business rules you can unit-test without infrastructure, frameworks you can swap without a rewrite, and a codebase whose structure screams its purpose rather than its framework.
Common Pitfalls
1Business Logic Trapped in Controllers
A 'quick' API put all the order logic directly in the Express route handlers, with raw SQL inline. Now the rules can only run inside an HTTP request, can't be reused by a background job, and unit tests need a running web server and database.
There's no separation between delivery (HTTP), application rules, and data access. The business logic is coupled to the framework and the database, so it isn't reusable or testable in isolation — every change risks the web layer.
Extract use-case classes (PlaceOrder, CancelOrder) that depend on repository interfaces. Controllers become thin: parse input, call the use case, format output. The same use cases now run from a queue worker or CLI, and unit tests exercise them with in-memory repositories — no HTTP, no DB.
Takeaway: Keep business rules independent of the delivery mechanism. Thin controllers calling framework-agnostic use cases make logic reusable across HTTP/jobs/CLI and trivially testable.
2Swapping the Database Without a Rewrite
A product needs to migrate from a SQL database to a document store for one bounded context, but the team fears it means touching business logic everywhere.
In a typical layered app where services call the ORM directly, the persistence technology leaks into the business code, so a storage change ripples through the whole codebase.
Because the core only depends on repository interfaces (ports), they write a new adapter (MongoOrderRepository) implementing the same interface and change one line at the composition root. The use cases and entities are untouched — the database really was just a detail.
Takeaway: When the core depends on abstractions it owns, infrastructure becomes swappable. Clean architecture's payoff is exactly this: changing a 'detail' like the database doesn't change the business rules.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.