Menu
Course/Architectural Styles/Clean Architecture

Clean Architecture

Uncle Bob's Clean Architecture: the dependency rule, entities, use cases, interface adapters, and frameworks. Practical application beyond the theory.

15 min read

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.

Loading diagram...
Clean Architecture's four rings — all dependencies point inward toward entities

The Four Rings

RingNameContentsChange frequency
1 (innermost)EntitiesCore 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
2Use CasesApplication-specific business rules. Orchestrate Entities to fulfill a use case. E.g., PlaceOrderUseCase, RefundPaymentUseCase.Sometimes — when business requirements change
3Interface AdaptersConvert data between Use Case format and external format. Controllers, Presenters, Repository implementations.Often — when UI or external APIs change
4 (outermost)Frameworks & DriversWeb frameworks (Express, Rails), database drivers, UI toolkits. Just glue code.Most often — technology choices evolve

The Dependency Rule in Practice

typescript
// 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.

📝

Knowledge Check

5 questions

Test your understanding of this lesson. Score 70% or higher to complete.

Ask about this lesson

Ask anything about Clean Architecture