Clean Architecture

Clean Architecture

"The Dependency Rule: Source code dependencies can only point inwards. Nothing in an inner circle can know anything about something in an outer circle." — Robert C. Martin, 2012

Intent

Clean Architecture is Robert C. Martin's 2012 formulation of the dependency inversion principle applied to application architecture. The Dependency Rule is the ONLY rule — everything else (four concentric circles, specific layer names) is illustrative, not prescriptive. The rule is: source code dependencies must point inward.

Clean, Hexagonal, and Onion express the same foundational principle with different vocabularies — three separate notes exist because teams encounter these patterns by name and the vocabulary differences matter in practice. A developer working in a codebase that uses the term "Use Cases" needs the Clean Architecture vocabulary, not a merged description.

Martin's companion concept — "Screaming Architecture" — holds that package names should scream use cases, not frameworks. com.app.ordering communicates domain intent; com.app.services communicates nothing. The folder/package structure is itself a design decision.

When NOT to Use

  • Simple CRUD applications with no significant business rules — the Use Case abstraction overhead exceeds its value when there is no business logic to isolate
  • Small scripts and utilities
  • Teams early in DDD adoption who have not yet identified bounded contexts — the Entities vs Use Cases distinction is only meaningful once domain boundaries are clear
  • Prototypes where the cost of layer separation outweighs the benefit of testability

When to Use

  • Systems with non-trivial business rules that must be tested independently of the UI and database
  • Teams that need to swap persistence or delivery mechanisms without rewriting business logic
  • When "screaming architecture" is desired — a package structure that communicates use cases, not frameworks
  • Long-lived systems where framework churn is expected (frameworks change; business rules should not)

How It Works

Clean Architecture defines four concentric layers (inner to outer):

  1. Entities — enterprise-wide business rules; pure domain objects. An Order that validates its own invariants is an Entity. This rule applies in any application using this domain — it is not specific to one workflow.
  2. Use Cases — application-specific business rules; orchestrate Entities to fulfil one workflow. PlaceOrderUseCase coordinates Order, Payment, and Inventory for THIS application's specific workflow. Use Cases define the interfaces (ports) that outer layers must implement.
  3. Interface Adapters — Controllers, Presenters, Gateways. This layer translates between the format convenient for Use Cases and the format convenient for Frameworks. A Controller translates an HTTP request into a command; a Gateway translates a Use Case's repository interface into SQL.
  4. Frameworks & Drivers — Web, DB, UI, external services. The outermost ring. All framework coupling lives here. Database drivers, HTTP servers, and UI components are details — they are plugged in from outside.

The Dependency Rule (precise wording): "Source code dependencies may only point inward. Nothing in an inner circle can know anything about something in an outer circle."

This means: Entities know nothing about Use Cases. Use Cases know nothing about Interface Adapters. Interface Adapters know nothing about Frameworks. The Order entity has no import of a JPA annotation. The PlaceOrderUseCase has no import of express.Request. Violations of this rule break the architecture.

+---------------------------------------+
|         Frameworks & Drivers          |
|   +---------------------------+       |
|   |     Interface Adapters    |       |
|   |   +---------------+       |       |
|   |   |   Use Cases   |       |       |
|   |   | +---------+   |       |       |
|   |   | | Entities|   |       |       |
|   |   | +---------+   |       |       |
|   |   +---------------+       |       |
|   +---------------------------+       |
+---------------------------------------+
            Dependencies --> inward
Confusion with Hexagonal and Onion

All three architectures express the same foundational principle — source code dependencies must point inward toward the domain — but use distinct vocabularies. Teams use specific terminology; the vault must match what they will encounter in any given codebase.

DimensionHexagonal (Cockburn, 2005)Onion (Palermo, 2008)Clean (Martin, 2012)
Layer NamesPort / Adapter / Application CoreDomain Model / Domain Services / Application Services / InfrastructureEntities / Use Cases / Interface Adapters / Frameworks & Drivers
Dependency Rule StatementAll outside actors connect via ports; application core has no outward dependenciesAll coupling toward the centre; outer layers depend on inner layersSource code dependencies can only point inward
What It Prescribes Inside the CoreNothing — the hexagon's internals are not specifiedExplicitly separates Domain Services from Application ServicesExplicitly separates Entities from Use Cases

Architecture Diagram

Clean-Architecture-diagram.excalidraw

TypeScript Example

Folder layout showing WHERE code lives and which direction dependencies point:

src/
  domain/           # Entities (no external imports)
    Order.ts
  application/      # Use Cases
    ports/
      OrderRepository.ts    # interface — defined here, implemented outside
    PlaceOrderUseCase.ts
  adapters/         # Interface Adapters
    controllers/
      OrderController.ts
    persistence/
      PostgresOrderRepository.ts
  infrastructure/   # Frameworks & Drivers
    db/
    http/

Key interface pattern — the dependency rule enforced by types:

// Clean Architecture — TypeScript
 
// src/application/ports/OrderRepository.ts
// Interface defined in Use Cases ring — NOT in infrastructure
export interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}
 
// src/application/PlaceOrderUseCase.ts
// Use Case imports ONLY from domain and ports — no framework or DB imports
import { Order } from '../domain/Order';
import { OrderRepository } from './ports/OrderRepository';
 
export class PlaceOrderUseCase {
  constructor(private readonly orders: OrderRepository) {}
 
  async execute(command: PlaceOrderCommand): Promise<void> {
    const order = Order.place(command.items, command.customerId);
    await this.orders.save(order);
  }
}

Java Example

Package layout:

com.app/
  domain/           # Entities
    Order.java
  application/      # Use Cases
    ports/
      OrderRepository.java   # interface defined here
    PlaceOrderUseCase.java
  adapters/         # Interface Adapters
    controllers/
      OrderController.java
    persistence/
      JpaOrderRepository.java
  infrastructure/   # Frameworks & Drivers
    config/

Key interface pattern:

// Clean Architecture — Java
 
// application/ports/OrderRepository.java — interface in Use Cases ring
public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    void save(Order order);
}
 
// application/PlaceOrderUseCase.java — no Spring, no JPA imports
public class PlaceOrderUseCase {
    private final OrderRepository orders;  // Injected at composition root
 
    public PlaceOrderUseCase(OrderRepository orders) {
        this.orders = orders;
    }
 
    public void execute(PlaceOrderCommand command) {
        Order order = Order.place(command.items(), command.customerId());
        orders.save(order);
    }
}

Enforcement Tooling

Stating the Dependency Rule without automated enforcement leaves it aspirational. Violations accumulate silently. Use these tools to make the rule a failing build check.

TypeScript — dependency-cruiser

npm install --save-dev dependency-cruiser
// .dependency-cruiser.js
{
  forbidden: [{
    name: "no-domain-to-infrastructure",
    from: { path: "^src/domain" },
    to:   { path: "^src/(adapters|infrastructure)" }
  }, {
    name: "no-application-to-infrastructure",
    from: { path: "^src/application" },
    to:   { path: "^src/infrastructure" }
  }]
}

Java — ArchUnit

<!-- Maven: pom.xml -->
<dependency>
  <groupId>com.tngtech.archunit</groupId>
  <artifactId>archunit-junit5</artifactId>
  <version>1.3.0</version>
  <scope>test</scope>
</dependency>
@AnalyzeClasses(packages = "com.app")
class CleanArchitectureTest {
    @ArchTest
    static final ArchRule domainMustNotDependOnAdapters =
        noClasses().that().resideInAPackage("..domain..")
            .should().dependOnClassesThat()
            .resideInAPackage("..adapters..");
 
    @ArchTest
    static final ArchRule applicationMustNotDependOnInfrastructure =
        noClasses().that().resideInAPackage("..application..")
            .should().dependOnClassesThat()
            .resideInAPackage("..infrastructure..");
}

Common dependency rule violations to watch for:

  • JPA annotations (@Entity, @Column) placed directly on domain entity classes — these import javax.persistence or jakarta.persistence into the Entities ring
  • Framework imports (import { Injectable } from '@nestjs/common') inside use case classes — the Use Cases ring must not import framework modules
  • Logging framework imports (import org.slf4j.Logger) inside domain services — domain logic must not depend on a specific logging implementation
  • Direct express.Request import inside a use case — HTTP types belong in the Interface Adapters ring, not in Use Cases
PatternRelationship
Hexagonal-ArchitectureCockburn's equivalent formulation (2005) using Port/Adapter vocabulary; both enforce the same Dependency Rule with different layer names
Onion-ArchitecturePalermo's equivalent formulation (2008) with explicit Domain Services vs Application Services split; the Use Cases layer in Clean maps to two separate rings in Onion
Adapter-PatternThe GoF Adapter is the structural mechanism that Interface Adapters use — each adapter translates a framework type to the interface the Use Cases expect
RepositoryThe canonical dependency inversion example: Repository interface defined in Use Cases ring, implementation in Infrastructure — the same inversion the Dependency Rule enforces throughout
Bounded-ContextBounded Contexts define the domain boundaries within which a Clean Architecture is valid; the Entities and Use Cases layers align with one context's domain model
CQRS-PatternCommands and Queries map to Use Case classes; CQRS is the application-level decomposition of what Clean Architecture calls the Use Cases layer
Facade-PatternInterface Adapters expose Facade semantics toward the outer ring — they simplify the Use Cases API for controllers and presenters

Sources