Clean Architecture
The Dependency Rule: the core never knows about the outside
Formalized by Robert C. Martin ("Uncle Bob") in 2012, Clean Architecture sets one non-negotiable rule: source code dependencies can only point inward. Everything else - the four circles, the layer names - is just one concrete way of applying that rule.
Frameworks & Drivers
web, DB, UI
Interface Adapters
controllers, presenters
Use Cases
business orchestration
Entities
business rules
Source code dependencies point only inward.
Origin and goal
In 2012, Robert Martin published a blog post titled "The Clean Architecture", noticing that several architectures that appeared independently - Cockburn's hexagonal architecture, Jeffrey Palermo's Onion Architecture, DCI - all converge toward the same goals.
Those goals: a system testable without a UI, without a database, without a web server; a database and a framework that stay details, replaceable without touching business rules; business rules that ignore the outside world entirely.
Rather than inventing a new idea, Martin offered a unified representation - four concentric circles - and a name for the rule that ties them together: the Dependency Rule.
The Dependency Rule
The Dependency Rule states: source code in a circle can only reference elements in the same circle or in a more inner one. Nothing in an inner circle should know the name of a class, function, or variable defined in an outer circle.
This includes data formats. If the inner circle needs data produced by an outer circle, you don't pass a framework structure directly: you define a plain interface, owned by the inner circle, and the outer circle implements it. That's the dependency inversion principle applied to the entire architecture.
The four circles
The original diagram distinguishes four circles, from innermost to outermost:
- Entities: the most general business objects and rules of the enterprise, the ones that would survive even a complete change of application. They depend on nothing.
- Use Cases: application-specific orchestration - "place an order", "cancel a subscription". They use entities and define, as interfaces, what they need from the outside.
- Interface Adapters: controllers, presenters, mappers. They translate between the format convenient for use cases and the format convenient for external technology (HTTP, SQL, JSON).
- Frameworks & Drivers: the outermost layer - database, web framework, UI. Code here is kept to a minimum, mostly configuration and wiring.
How does it compare to hexagonal architecture?
Both approaches defend exactly the same idea with different vocabulary. The hexagonal core (domain) maps to Clean Architecture's two inner circles (Entities + Use Cases). Hexagonal ports map to the interfaces that use cases define to talk to the outside.
The real added value of Clean Architecture is naming the "Interface Adapters" layer explicitly as its own thing - whereas hexagonal often leaves it merged with adapters. This intermediate layer (controllers, presenters, mappers) has the sole job of translating formats, carrying no business rule of its own.
In practice, a single project can claim both: "hexagonal architecture organized around Clean Architecture's dependency rule" describes most serious codebases that follow these principles quite well.
Concrete example: the same use case, in explicit layers
Take the "place an order" use case again. In Clean Architecture, you explicitly separate the entity (carrying intrinsic validation rules) from the use case (the interactor, which orchestrates) and from the port it defines for persistence.
// Entity: intrinsic business rule, zero external dependency
class Order private constructor(
val id: OrderId,
private val items: List<OrderLine>,
private val status: OrderStatus,
) {
companion object {
fun create(id: OrderId, items: List<OrderLine>): Order {
if (items.isEmpty()) throw EmptyOrderError()
return Order(id, items, OrderStatus.Pending)
}
}
fun totalAmount(): Money =
items.fold(Money.zero()) { sum, line -> sum.add(line.subtotal()) }
}// Use case: orchestration, defines what it needs through an interface
interface OrderGateway {
suspend fun save(order: Order)
}
class PlaceOrderInteractor(private val orders: OrderGateway) : PlaceOrder {
override suspend fun execute(command: PlaceOrderCommand): OrderId {
val order = Order.create(OrderId.generate(), command.items)
orders.save(order)
return order.id
}
}Common pitfalls
Three mistakes come up often when adopting Clean Architecture:
- Anemic entities: plain structures with getters/setters and no business rule at all, when the entity should carry the invariants (like rejecting an empty order in the example above).
- Framework annotations on entities (JPA, Mongoose, ORM decorators): this makes the innermost circle depend on an external library, directly violating the Dependency Rule.
- Stacking all four circles literally, with four folders and as many files, on an application that really has two or three simple business rules: the Dependency Rule matters, not the folder count.