Steven's Knowledge

Clean Architecture

Dependency direction as the central rule — concentric layers, the Dependency Rule, and where the ceremony does and does not earn its cost

Clean Architecture

Clean Architecture is Robert C. Martin's 2012 synthesis of patterns that had been around for decades — Hexagonal Architecture, Onion Architecture, Screaming Architecture, BCE — into a single layered model with one organizing principle: dependencies point inward, never outward. The business logic at the center knows nothing about the database, the web framework, the UI, or the platform. They depend on it.

When the Dependency Rule is followed, you can change frameworks, swap databases, replace the UI, and substitute in test doubles, all without touching the business logic. When it is violated, you get the system most teams have: business logic intertwined with the ORM, controllers full of domain rules, a "model" layer that is half-data-access half-business-rules.

This is a powerful idea applied to too many systems. The ceremony costs are real and not every codebase earns them back. This page is about both the rules and where they pay off.

The Concentric Circles

Clean Architecture organizes code into concentric layers:

   ┌─────────────────────────────────────────────────┐
   │ Frameworks & Drivers                             │
   │ (Web framework, DB driver, UI framework)         │
   │  ┌──────────────────────────────────────────┐   │
   │  │ Interface Adapters                        │   │
   │  │ (Controllers, Gateways, Presenters)       │   │
   │  │  ┌──────────────────────────────────┐    │   │
   │  │  │ Use Cases                          │   │   │
   │  │  │ (Application-specific business     │   │   │
   │  │  │  rules)                            │   │   │
   │  │  │  ┌──────────────────────────┐     │   │   │
   │  │  │  │ Entities                   │   │   │   │
   │  │  │  │ (Enterprise-wide           │   │   │   │
   │  │  │  │  business rules)           │   │   │   │
   │  │  │  └──────────────────────────┘     │   │   │
   │  │  └──────────────────────────────────┘    │   │
   │  └──────────────────────────────────────────┘   │
   └─────────────────────────────────────────────────┘
   
   Dependency direction: ───▶ (always inward)

Each layer has a specific role:

  • Entities (innermost). Enterprise-wide business rules. The most general, most stable. A Customer who knows what discounts they are eligible for; an Order that knows when it is valid. These do not change because of UI or database changes.
  • Use Cases. Application-specific business rules. "Place an order," "Issue a refund." Orchestrates entities to accomplish one user-visible operation. Knows nothing about HTTP, GraphQL, or the database.
  • Interface Adapters. Translates between the inner and outer worlds. Controllers (HTTP → use case calls), gateways (use case calls → database queries), presenters (use case results → view models).
  • Frameworks & Drivers (outermost). The web framework, the database driver, the UI framework, the message broker SDK. Volatile, replaceable.

The Dependency Rule

Source code dependencies must point only inward, toward higher-level policies.

An entity can be referenced by a use case. A use case can be referenced by an interface adapter. An interface adapter can be referenced by the framework layer. The reverse — an entity importing the database driver, a use case knowing about HTTP — violates the rule.

When the inner layer needs the outer layer (the use case needs to save an order), the dependency is inverted. The use case defines an interface (OrderRepository); the outer layer implements it (PostgresOrderRepository). The use case depends on the interface, which it owns; the outer layer depends on the use case's interface. Dependencies still point inward.

// Inner: domain owns this interface
interface OrderRepository {
  save(order: Order): Promise<void>
  findById(id: OrderId): Promise<Order | null>
}

// Outer: infrastructure depends on the inner interface
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> { /* SQL */ }
  async findById(id: OrderId): Promise<Order | null> { /* SQL */ }
}

// Use case depends on the interface, not the implementation
class PlaceOrderUseCase {
  constructor(private repo: OrderRepository) {}
  async execute(req: PlaceOrderRequest): Promise<Order> { /* ... */ }
}

The composition root (your main) wires the concrete implementation into the use case at startup. Nothing in the use case knows the database exists.

What Clean Architecture Buys

The benefits, where they are real:

  • Framework independence. Switching from Express to Fastify is a controller-layer change. The use cases and entities do not know.
  • Database independence. Switching from Postgres to DynamoDB is a repository-layer change. Same story.
  • UI independence. The same use cases can be driven by a CLI, a REST API, a GraphQL endpoint, a gRPC service, or a background job.
  • Testability. Use cases can be tested with in-memory implementations of the interfaces they depend on. No database fixtures, no HTTP server, no framework lifecycle. Tests run in milliseconds and never flake.
  • Boundaries make intent visible. A reviewer can see immediately what is business logic and what is infrastructure. A new engineer has a roadmap of the codebase.

What Clean Architecture Costs

The honest other side:

  • Ceremony. Each operation typically touches a request DTO, a controller method, a use case class, an entity, a repository interface, a repository implementation, and a response DTO. Seven files for a "create user" endpoint that would have been one file in a Rails-style framework.
  • Indirection. Reading the code requires following interface references across packages. A debugger user has to step through layers that contain no business meaning.
  • Wrong default for many apps. Simple CRUD apps, content management, internal tools — for these, the layered ceremony costs more than it saves. The framework's defaults plus a thin domain are often the better answer.
  • Anti-corruption tax. Every external system needs an adapter layer (Anticorruption Layer). Even when the external model is fine for your purposes, the architecture pushes you to wrap it.
  • Overkill in small teams. With 3 engineers and one product, the layered ceremony adds friction without solving a problem.

When Clean Architecture Is Worth It

The signals:

  • The domain is genuinely complex. Many business rules, evolving requirements, regulatory constraints. The rules deserve a clean home; the layered model gives them one.
  • The system will live long. Long-lived code outlasts frameworks, databases, and UI choices. Clean Architecture's separation pays back when you do swap.
  • Multiple delivery channels are expected. The same use cases will be invoked from API, batch job, CLI, mobile backend. The use-case layer is the natural composition point.
  • Testing matters enough to design for. If you intend to have a fast test suite that exercises business rules without external dependencies, the layered model makes it easy.
  • Team will grow. Boundaries and conventions help when more people are committing. A consistent layered structure scales the onboarding cost.

When Clean Architecture Is Not Worth It

  • Simple CRUD apps. A user signs up, edits their profile, lists their items. The framework's MVC defaults plus a thin domain model is the right answer.
  • Small team, short timeline. A prototype to validate a market does not need Clean Architecture. Ship, learn, refactor later.
  • The domain is shallow. If your business logic is "save what the user typed," there is no domain layer to defend.
  • You will use the framework's features extensively. Heavy reliance on framework conventions (Rails' Active Record, Django's models, Phoenix's contexts) means the framework wants to be at the center — Clean Architecture fights this and produces a worse-of-both-worlds outcome.

Relationship to Hexagonal and Onion

Clean Architecture is Bob Martin's synthesis of:

  • Hexagonal Architecture (Cockburn, 2005) — Ports and Adapters. Provides the actual translation mechanism between domain and external systems.
  • Onion Architecture (Palermo, 2008) — Concentric layers with dependency direction.
  • BCE (Jacobson, 1990s) — Boundary, Control, Entity from Object-Oriented Software Engineering.
  • DCI (Reenskaug & Coplien) — Data, Context, Interaction.

The three modern presentations — Clean, Hexagonal, Onion — are close enough that practitioners often use the names interchangeably. The differences are emphasis (Hexagonal stresses ports, Onion stresses layering, Clean synthesizes both) rather than substance. Pick the framing that explains your design best.

Common Mistakes

  • Layers without dependency inversion. The "outer" layer imports the "inner" layer's concrete types. The diagram looks right; the dependencies do not. This is just layering without the actual benefit.
  • Anemic use cases. Use cases that only forward to a single repository call. The use-case layer adds ceremony without value; the business logic lives elsewhere or does not exist.
  • Entities used as data classes. Entities have only fields; behavior lives in services. This is the same anemic-domain anti-pattern from DDD.
  • DTOs everywhere. Five DTOs for every operation (request, command, entity, response, view model). Some of these are useful; five of them is ceremony.
  • Re-implementing the framework. Clean Architecture's "framework independence" is misread as "do not use framework features." You can use the framework's routing, validation, and serialization; just keep them out of the entity layer.
  • Adopting it because Bob Martin said so. The pattern has costs. Apply it where the costs are repaid by domain complexity, longevity, or testability requirements.

Relation to Other Pages

  • Hexagonal Architecture — the closely-related sibling; ports & adapters is the practical mechanism inside Clean Architecture.
  • Onion Architecture — the third name for substantially the same idea.
  • DDD Tactical Patterns — Entities in Clean Architecture are DDD aggregates; Use Cases sit alongside Domain Services.
  • Modular Monolith — Clean Architecture organizes a module's internals; modular monolith organizes the modules.
  • code-craft/design/design-principles — SOLID, especially Dependency Inversion, is the code-level discipline that makes the Dependency Rule possible.

Further Reading

  • Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017) — the book-length treatment.
  • Robert C. Martin, The Clean Architecture (blog post, 2012) — the original short article and the famous diagram.
  • Alistair Cockburn, Hexagonal Architecture (2005) — the earlier formulation that Clean Architecture extends.
  • Jeffrey Palermo, The Onion Architecture (blog series, 2008) — the parallel formulation.
  • Mark Seemann, Code that Fits in Your Head — pragmatic guide to layered architecture with sensible defaults about when to apply it.

Pre-commit Checklist

  • For each module I am building with Clean Architecture, can I name a specific reason (domain complexity, longevity, multiple channels, test discipline) that justifies the ceremony cost?
  • Do dependencies actually point inward — or has an inner layer accidentally imported an outer one?
  • Are my entities defined by their business behavior, not their database shape?
  • Are my use cases doing real orchestration, or are they thin pass-throughs to repositories?
  • Can my use cases be tested without spinning up a database, HTTP server, or framework?
  • Have I avoided creating ceremony — DTOs, interfaces, layers — that does not earn its place?

On this page