Clean Architecture
Uncle Bob's Clean Architecture: the dependency rule, entities, use cases, interface adapters, and frameworks. Practical application beyond the theory.
Clean Architecture Overview
Clean Architecture, introduced by Robert C. Martin (Uncle Bob) in 2012, formalizes the principle of separating software into concentric rings where the innermost ring contains the most stable business rules and the outermost ring contains the most volatile infrastructure details. It is a synthesis of hexagonal architecture, onion architecture, and DCI (Data, Context, Interaction) architecture.
The central law is the Dependency Rule: source code dependencies can only point inward. Inner rings define interfaces; outer rings implement them. Nothing in an inner ring can know anything about an outer ring.
The Four Rings
| Ring | Name | Contents | Change frequency |
|---|---|---|---|
| 1 (innermost) | Entities | Core business objects with enterprise-wide rules. E.g., an Invoice entity with validation rules that apply regardless of application. | Rarely — rules are fundamental to the business |
| 2 | Use Cases | Application-specific business rules. Orchestrate Entities to fulfill a use case. E.g., PlaceOrderUseCase, RefundPaymentUseCase. | Sometimes — when business requirements change |
| 3 | Interface Adapters | Convert data between Use Case format and external format. Controllers, Presenters, Repository implementations. | Often — when UI or external APIs change |
| 4 (outermost) | Frameworks & Drivers | Web frameworks (Express, Rails), database drivers, UI toolkits. Just glue code. | Most often — technology choices evolve |
The Dependency Rule in Practice
// Ring 1: Entity — pure business object, no imports from outer rings
export class Invoice {
constructor(
public readonly id: string,
public readonly lineItems: LineItem[],
) {}
get total(): number {
return this.lineItems.reduce((sum, item) => sum + item.subtotal, 0);
}
validate(): void {
if (this.lineItems.length === 0) throw new Error("Invoice must have items");
if (this.total <= 0) throw new Error("Invoice total must be positive");
}
}
// Ring 2: Use Case — defines what it needs via interfaces (inward dependencies only)
interface InvoiceRepository {
save(invoice: Invoice): Promise<void>;
findById(id: string): Promise<Invoice | null>;
}
interface EmailNotifier {
sendInvoice(email: string, invoice: Invoice): Promise<void>;
}
export class CreateInvoiceUseCase {
constructor(
private readonly repo: InvoiceRepository, // interface, not implementation
private readonly notifier: EmailNotifier, // interface, not implementation
) {}
async execute(request: CreateInvoiceRequest): Promise<string> {
const invoice = new Invoice(generateId(), request.lineItems);
invoice.validate();
await this.repo.save(invoice);
await this.notifier.sendInvoice(request.customerEmail, invoice);
return invoice.id;
}
}
// Ring 3: Interface Adapter — implements the repository interface from Ring 2
export class PostgresInvoiceRepository implements InvoiceRepository {
async save(invoice: Invoice): Promise<void> {
await db.query("INSERT INTO invoices ...", [invoice.id, invoice.total]);
}
// ...
}
// Ring 4: Framework — wires it together (Express route)
app.post("/invoices", async (req, res) => {
const useCase = new CreateInvoiceUseCase(
new PostgresInvoiceRepository(db),
new SendGridEmailNotifier(sgClient),
);
const id = await useCase.execute(req.body);
res.status(201).json({ id });
});Crossing the Boundary: The Humble Object Pattern
When data must flow outward (e.g., a use case returns data to a controller), Clean Architecture uses Data Transfer Objects (DTOs) at the boundary. The use case does not return domain entities to the controller — it returns simple data structures (plain objects, value objects). This prevents outer rings from depending on inner-ring entity implementations.
Don't Over-Engineer Small Projects
Clean Architecture adds indirection and boilerplate. For a small CRUD service with no complex business rules, a simple three-layer structure (controller → service → repository) is more pragmatic. Clean Architecture pays dividends when: domain logic is complex and evolves frequently, you want to delay technology decisions (which database? which framework?), and when testing domain logic in isolation is critical.
Clean Architecture vs Hexagonal Architecture
Both architectures enforce the Dependency Inversion Principle as a core mechanism. The key differences: Clean Architecture gives names to the inner rings (Entities, Use Cases) and is more prescriptive about layering inside the core. Hexagonal architecture focuses on the ports-and-adapters mechanism without prescribing internal core structure. In practice, you will often see teams combine both: hexagonal for the outside boundary, clean architecture for the internal ring structure.
Interview Tip
When asked about Clean Architecture, lead with the Dependency Rule: 'All source code dependencies point inward — nothing in the domain knows about the framework or database.' Then explain the four rings. If you have used it in a project, mention concrete benefits: we could switch from PostgreSQL to MongoDB for one service with zero changes to the use case layer. That concreteness impresses interviewers more than textbook definitions.