SOFTWARE ARCHITECTURE / SYSTEM CONCEPT BRIEF

Clean Architecture

Clean Architecture, popularized by Robert C.

IntermediatePhase 08 / Topic 4 of 17RequirementsTrade-offsFailure modes
01

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.

A power adapter

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.

02

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.
03

Where it shows up in interviews

Service internal design

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
04

Where it is used in real software

Hexagonal architecture

Alistair Cockburn's ports and adapters pattern, widely used in Java and .NET services.

Android architecture guidance

Recommends domain, data, and UI layers with use cases and repositories.

NestJS and Spring projects

Commonly structure modules with domain, application, and infrastructure folders.

05

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.
06

How it works, step by step

  1. 1
    Model the domain

    Entities with business rules, no framework imports.

  2. 2
    Write use cases

    PlaceOrder, RefundPayment, depending only on ports.

  3. 3
    Define ports

    Interfaces for persistence, messaging, external services.

  4. 4
    Implement adapters

    PostgresOrderRepository, StripePaymentGateway.

  5. 5
    Wire at the edge

    Composition root injects adapters into use cases.

07

Layers and dependencies

Where code goes

Step 1 / 4
LayerContainsMay depend on
EntitiesOrder, Money, business invariantsNothing
Use casesPlaceOrder, CancelOrderEntities, ports
Interface adaptersControllers, repositories, mappersUse cases, ports
Frameworks and driversExpress, Spring, PostgreSQL, KafkaAdapters

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.

08

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()]);  }}
09

Complexity and performance

Extra abstractionInterfaces per external dependency

Small cost.

Unit test speedMilliseconds

No infrastructure.

10

Trade-offs

Flexibility vs boilerplate

Clear layering and testability, at the cost of more interfaces and mapping code, which can be overkill for simple CRUD.

Purity vs pragmatism

Strict layering can slow small teams; apply it where business logic is rich.

11

Variants and related techniques

Hexagonal (ports and adapters)

Same principle framed as inside vs outside.

Vertical slice architecture

Organize by feature, each slice with its own thin layers.

12

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.

13

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.

14

Practice problems

ProblemDifficultyWhat it trains
Refactor a controller-heavy endpoint into a use caseMediumLayering.
Design ports and adapters for a notification serviceMediumSwappable providers.