Overview
Clean Architecture, popularized by Robert C. Martin, organizes code into concentric layers where dependencies point only inward. At the center are entities (core business rules), then use cases (application-specific rules), then interface adapters (controllers, presenters, repositories), and on the outside frameworks and drivers (web framework, database, external APIs).
The key idea is the dependency rule: business logic never depends on frameworks, databases, or UI. Outer layers implement interfaces defined by inner layers (dependency inversion). This makes the core testable without infrastructure and lets you swap databases or frameworks with limited impact. Hexagonal architecture (ports and adapters) and onion architecture express the same idea.
Your laptop (business logic) has one standard port. Travel adapters (outer adapters) let it plug into any country's socket (database, framework). The laptop never changes; you just swap adapters.
When to use it
- Applications with significant business logic that must outlive frameworks.
- Codebases where testability is a priority.
- Teams wanting clear conventions for where code belongs.
- Services that integrate with many external systems.
Where it shows up in interviews
Recognize it when: how is the code inside a service structured?
- Design the code structure for a payment service
- LLD: design a parking lot with clean layers
Where it is used in real software
Alistair Cockburn's ports and adapters pattern, widely used in Java and .NET services.
Recommends domain, data, and UI layers with use cases and repositories.
Commonly structure modules with domain, application, and infrastructure folders.
Key terms
- Entities
- Enterprise-wide business rules and objects.
- Use cases / interactors
- Application-specific operations orchestrating entities.
- Ports
- Interfaces the core defines (OrderRepository, PaymentGateway).
- Adapters
- Implementations of ports for specific technologies.
- Dependency rule
- Source code dependencies point only inward.
How it works, step by step
- 1Model the domain
Entities with business rules, no framework imports.
- 2Write use cases
PlaceOrder, RefundPayment, depending only on ports.
- 3Define ports
Interfaces for persistence, messaging, external services.
- 4Implement adapters
PostgresOrderRepository, StripePaymentGateway.
- 5Wire at the edge
Composition root injects adapters into use cases.
Layers and dependencies
Where code goes
| Layer | Contains | May depend on |
|---|---|---|
| Entities | Order, Money, business invariants | Nothing |
| Use cases | PlaceOrder, CancelOrder | Entities, ports |
| Interface adapters | Controllers, repositories, mappers | Use cases, ports |
| Frameworks and drivers | Express, Spring, PostgreSQL, Kafka | Adapters |
NOWLayer: Entities | Contains: Order, Money, business invariants | May depend on: Nothing
You can test PlaceOrder with in-memory fakes, no database or HTTP server required.
Implementation
// Domain (no imports from frameworks)export class Order { constructor(readonly id: string, readonly items: { sku: string; qty: number; priceCents: number }[]) { if (items.length === 0) throw new Error("Order must have items"); } total() { return this.items.reduce((s, i) => s + i.qty * i.priceCents, 0); }} // Ports defined by the coreexport interface OrderRepository { save(order: Order): Promise<void> }export interface PaymentGateway { charge(orderId: string, cents: number): Promise<{ ok: boolean }> } // Use case depends only on portsexport class PlaceOrder { constructor(private orders: OrderRepository, private payments: PaymentGateway) {} async execute(id: string, items: Order["items"]) { const order = new Order(id, items); const result = await this.payments.charge(order.id, order.total()); if (!result.ok) throw new Error("Payment declined"); await this.orders.save(order); return order; }} // Adapter (outer layer)export class PgOrderRepository implements OrderRepository { constructor(private pool: { query: (sql: string, params: unknown[]) => Promise<unknown> }) {} async save(order: Order) { await this.pool.query("INSERT INTO orders (id, total_cents) VALUES ($1, $2)", [order.id, order.total()]); }}Complexity and performance
Small cost.
No infrastructure.
Trade-offs
Clear layering and testability, at the cost of more interfaces and mapping code, which can be overkill for simple CRUD.
Strict layering can slow small teams; apply it where business logic is rich.
Variants and related techniques
Same principle framed as inside vs outside.
Organize by feature, each slice with its own thin layers.
Common mistakes
- Domain entities importing ORM annotations or HTTP types.
Fix: Keep the core free of framework dependencies.
- Anemic domain models.
Fix: Put business rules in entities and use cases, not controllers.
Interview questions
What is the dependency rule?
Source code dependencies must point inward: business rules do not know about controllers, databases, or frameworks. Outer layers implement interfaces defined by inner layers.
How does clean architecture help testing?
Use cases depend on interfaces, so tests can inject in-memory fakes for repositories and gateways and verify business logic quickly without databases or network calls.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Refactor a controller-heavy endpoint into a use case | Medium | Layering. |
| Design ports and adapters for a notification service | Medium | Swappable providers. |