Architecture

Clean Architecture

Organizes the system into concentric layers where dependencies only point inward — frameworks and databases sit at the outer edge, business entities at the core — and explicit boundaries isolate each layer through dependency inversion.

Intent

Make implementation details — frameworks, databases, UI, devices — replaceable without affecting business logic. Clean Architecture, described by Robert C. Martin in 2012, formulates the dependency rule: source code in an inner layer must never mention anything from an outer layer. Dependencies only point inward.

The central point isn't the diagram of circles itself, but the rule it represents: business entities don't know about use cases; use cases don't know about controllers; controllers don't know about the database. Every boundary crossing uses an abstraction — an interface or a DTO — so that the inner layer defines the contract and the outer layer implements it.

Clean Architecture converges with Hexagonal Architecture and with Onion Architecture: all of them share the idea of an isolated domain at the center and infrastructure at the edges. The difference is in granularity: Clean Architecture explicitly details the separation between entities (pure business rules) and use cases (application rules), and turns boundaries (boundary-crossing interfaces) into a formal element of the model.

Problem

The problem Clean Architecture attacks is coupling to detail:

  • Coupling to the framework: when entities inherit from framework classes (ActiveRecord, JPA Entity), switching ORMs requires rewriting the domain. The framework should be a replaceable detail, not the foundation of the architecture.
  • Coupling to the database: business rules that depend on SQL, specific schemas or database transactions make the domain untestable without real infrastructure.
  • Coupling to the UI: business logic scattered across controllers, UI components or HTTP handlers that need to be duplicated whenever a new entry channel (CLI, queue, GraphQL API) is added.
  • Compromised testability: when a simple business rule ("the maximum discount is 30%") can only be verified by spinning up an HTTP server with a database, the cost of testing is high and the feedback loop is slow.

Structure

Clean Architecture is represented as concentric circles. Every outer circle is a detail; every inner circle is a policy. The rule is simple: an inner circle can never depend on an outer circle.

  ╔═══════════════════════════════════════════════════════════════════╗
  ║  FRAMEWORKS & DRIVERS (outermost edge)                            ║
  ║  Web, databases, UI, devices, external services                   ║
  ║  ┌─────────────────────────────────────────────────────────────┐  ║
  ║  │  INTERFACE ADAPTERS                                         │  ║
  ║  │  Controllers, Presenters, Gateways, Serializers             │  ║
  ║  │  ┌───────────────────────────────────────────────────────┐  │  ║
  ║  │  │  USE CASES (Application Business Rules)               │  │  ║
  ║  │  │  Use Case Interactors, Application Services           │  │  ║
  ║  │  │  ┌─────────────────────────────────────────────────┐  │  │  ║
  ║  │  │  │  ENTITIES (Enterprise Business Rules)           │  │  │  ║
  ║  │  │  │  Entities, Value Objects, Business Rules        │  │  │  ║
  ║  │  │  │  Pure — no dependency on anything external      │  │  │  ║
  ║  │  │  └─────────────────────────────────────────────────┘  │  │  ║
  ║  │  └───────────────────────────────────────────────────────┘  │  ║
  ║  └─────────────────────────────────────────────────────────────┘  ║
  ╚═══════════════════════════════════════════════════════════════════╝

  Dependency rule: arrows point ONLY inward.
  Entities don't know about Use Cases.
  Use Cases don't know about Controllers.
  Controllers don't know about Frameworks directly (they use interfaces).

The four layers

  • Entities: encapsulate the most general, highest-level business rules of the enterprise. They're the objects that would exist even without any software — the policies that hold for any system implementing that domain. They depend on nothing external.
  • Use cases: contain the application-specific business rules. They orchestrate the flow of data to and from the entities, and direct the entities to use their business rules to achieve the use case's goal. They know entities, but don't know controllers or the database.
  • Interface adapters: convert data between the format most convenient for use cases and entities and the format most convenient for some external agent like a database or the web. Controllers, Presenters and Gateways live here.
  • Frameworks and drivers: the outermost layer. This is where the details live: the web framework, the database, email services. You shouldn't write much code here — just glue code that communicates inward to the next layer.

Boundaries and DTOs

When data crosses a boundary between layers, it must do so through simple data structures — DTOs (Data Transfer Objects) — and never through inner-layer entities. This guarantees the outer layer doesn't drag dependencies inward.

The crossing happens through a boundary: an interface defined by the inner layer that the outer layer implements. The use case defines the repository interface it needs; the infrastructure implements that interface. The use case never imports the concrete implementation.

How it works — the dependency rule in practice

The snippet below shows the dependency rule applied to a simple use case. The repository interface is defined inside the use-case layer; the concrete implementation lives in the infrastructure and points inward — never the other way around.

// ── ENTITY (innermost layer) ─────────────────────────────────
// No external imports. Only pure business rules.
class Order {
  constructor(
    readonly id: string,
    readonly items: OrderItem[],
  ) {}

  calculateTotal(): number {
    return this.items.reduce((acc, item) => acc + item.subtotal(), 0);
  }

  validate(): void {
    if (this.items.length === 0) {
      throw new Error('Order must have at least one item');
    }
  }
}

// ── USE CASE (knows Entity; defines its own interface) ────────
// OutputBoundary is defined here — the outer layer implements it.
interface CreateOrderOutputBoundary {
  present(result: { orderId: string; total: number }): void;
}

interface OrderRepository {
  save(order: Order): Promise<void>;
}

class CreateOrderUseCase {
  constructor(
    private readonly repo: OrderRepository,
    private readonly output: CreateOrderOutputBoundary,
  ) {}

  async execute(dto: { items: { productId: string; quantity: number; price: number }[] }): Promise<void> {
    const order = new Order(
      crypto.randomUUID(),
      dto.items.map(i => new OrderItem(i.productId, i.quantity, i.price)),
    );
    order.validate();
    await this.repo.save(order);
    this.output.present({ orderId: order.id, total: order.calculateTotal() });
  }
}

// ── INTERFACE ADAPTER (Controller) ─────────────────────────────
// Knows the use case; doesn't know Entity directly.
class CreateOrderController {
  constructor(private readonly useCase: CreateOrderUseCase) {}

  async handle(req: Request): Promise<void> {
    await this.useCase.execute({ items: req.body.items });
  }
}

// ── INFRASTRUCTURE (outer edge) implements the inner interface ──
// The dependency points INWARD: Infra knows the Use Case.
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> {
    // accesses the database; the Use Case doesn't know this detail
  }
}

The critical point: OrderRepository and CreateOrderOutputBoundary are interfaces declared inside the use-case layer. PostgresOrderRepository lives in the infrastructure and imports the use case — the dependency points inward. The use case never imports PostgresOrderRepository.

When to use

  • Systems with a rich business domain and long longevity: the more complex the business rules and the longer the system's lifecycle, the more the investment in boundaries pays off. The ability to replace the database, the framework or the entry channel without touching the domain becomes more valuable over time.
  • When testability is a priority: the explicit separation of entities and use cases lets you test all the business logic with in-memory repository implementations — no database, no HTTP, running in milliseconds.
  • Multiple entry channels: the same set of use cases can be driven by HTTP, CLI, messaging or tests without modification, since each channel is just a different interface adapter.
  • Teams that need clear boundaries: the formal separation between layers — reinforced by boundaries — lets different teams work in different layers with minimal coupling.

When to avoid

  • Simple CRUDs without domain logic: creating entities, use cases, boundaries and DTOs for an application that only persists and reads records without complex rules is over-engineering with a real cost. A simple Layered Architecture, or even direct MVC, is more productive.
  • Short-lived projects or small teams without experience: Clean Architecture requires the team to understand dependency inversion, boundary DTOs and the distinction between entity and use case. Without that understanding, the result is usually a Layered Architecture with different names and redundant mappers without a clear purpose.
  • When boundary mappers become the largest volume of code: a sign the architecture may be applied with excessive granularity — mapping each field 1:1 between adjacent layers with no real transformation adds no value.

Pros and cons

Pros

  • Framework independence: the domain doesn't depend on any external library — frameworks are replaceable details.
  • Total testability of the domain without real infrastructure: entities and use cases are tested with simple interface mocks.
  • Database independence: switching from PostgreSQL to MongoDB (or to in-memory) means swapping the repository adapter, without touching the domain.
  • UI independence: the same business logic serves HTTP, CLI, tests and queues — each with its own adapter.
  • Formal boundaries make the contracts between layers explicit — they serve as living documentation of the architecture.

Cons

  • Larger code volume: boundary interfaces, input/output DTOs and mappers between layers multiply the file count even for simple features.
  • Learning curve: the distinction between entity, use case, adapter and framework — and the dependency inversion running through all of it — isn't intuitive the first time.
  • Risk of over-engineering for simple domains: applying four concentric layers with formal boundaries to a basic CRUD generates cost without an equivalent benefit.
  • Redundant mappers: with excessive granularity, the DTO converters between adjacent layers become the largest volume of code in the project without adding business logic.

Common pitfalls

1. Mapping folders 1:1 to the four layers and creating redundant mappers

The most common pitfall is creating four directories (entities/, use-cases/, adapters/, frameworks/) and mechanically converting every object between layers, even when there's no real transformation. If the use case's input DTO is structurally identical to the HTTP request payload, a mapper that copies field by field with no transformation adds no value — it only adds volume and maintenance complexity.

The dependency rule is what matters, not the number of directories. The folder structure should reflect the real dependency boundaries, not literally imitate the circle diagram.

2. Inverting the dependency in the wrong direction

The most serious and silent mistake: the use case imports the concrete repository implementation instead of the interface. This happens when the OrderRepository interface is declared in the infrastructure package and the use case imports that package. The dependency should be the other way around: the interface lives inside the use-case package, and the infrastructure imports the use case to implement the interface.

Quick test: if you delete the infrastructure folder, the entities and use-cases code should still compile without errors. If it doesn't compile, there's a dependency inverted in the wrong direction somewhere.

3. Over-engineering for CRUD

Applying Clean Architecture to a service that creates, reads, updates and deletes records without complex business rules generates a real development and maintenance cost with no equivalent benefit. The principle "the detail shouldn't influence the policy" only has value when there's a policy (business rule) to protect. For CRUDs, consider a simpler approach and refactor as the domain's complexity grows.

4. Ignoring the entities layer and putting everything in the use cases

When all the business rules end up in the use-case interactors and the entities are just data structures with no behavior, the result is an anemic domain — the same problem as a poorly applied Layered Architecture. Entities should encapsulate the most general and stable business rules; use cases orchestrate those entities to achieve application-specific goals.

Related architectures and patterns

Hexagonal Architecture is the architecture closest to Clean Architecture, and the two are often confused. The key difference is granularity: Hexagonal talks about ports (interfaces) and adapters (implementations), without formally distinguishing between entities and use cases inside the core. Clean Architecture adds that explicit distinction and formalizes boundaries as an element of the architecture. In practice, the two are compatible and convergent — a well-structured hexagonal implementation typically respects Clean Architecture's dependency rule.

Onion Architecture (Jeffrey Palermo, 2008) is the most direct precursor of Clean Architecture. It shares the idea of a domain at the center, dependencies pointing inward and infrastructure at the outer shell. The main difference is that Onion emphasizes the rich domain model at the core (entities with behavior, domain services) and tends to be less prescriptive about the separation between entities and use cases than Clean Architecture.

Layered Architecture is the simplest way to separate responsibilities horizontally. Clean Architecture refines it by explicitly inverting the dependency at the boundary between domain and infrastructure — instead of infrastructure sitting "below" as a natural dependency, it sits "outside" and implements interfaces defined by the domain.

The Adapter (GoF) pattern is the implementation mechanism of the interface adapters: every gateway, controller or presenter is an Adapter that converts between the inner layer's format and the external agent's format.