Home / Notebooks / System Design
System Design
intermediate

Clean Architecture

Core concepts from Robert C. Martin's guide to building systems where business rules are protected from frameworks, databases, and UI

April 28, 2026
Updated regularly

Clean Architecture

Robert C. Martin's central argument: the architecture of a system should make the business rules the most important thing — and make frameworks, databases, and UI irrelevant details.

A good architecture maximizes the number of decisions not yet made. The longer you can defer committing to a database, a framework, or a delivery mechanism, the more options you keep open.

---

Programming Paradigms

Martin frames architecture on top of three paradigms, each imposing a discipline by removing a capability:

ParadigmRemovesGives us
StructuredUnrestricted gotoProvable, testable control flow
Object-OrientedUnrestricted function pointersSafe polymorphism via dependency inversion
FunctionalUnrestricted assignment (mutable state)Predictable, side-effect-free code
These are tools for managing the three concerns of architecture: function, separation of components, and data management.

---

The Dependency Rule

The most important rule in Clean Architecture:

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

The inner layers know nothing about the outer layers. Names of things in the outer layers must not appear in the inner layers.

┌───────────────────────────────────────────────┐
│  Frameworks & Drivers (Web, DB, UI, Devices)  │
│  ┌─────────────────────────────────────────┐  │
│  │  Interface Adapters (Controllers,        │  │
│  │  Presenters, Gateways)                  │  │
│  │  ┌───────────────────────────────────┐  │  │
│  │  │  Use Cases (Application Rules)    │  │  │
│  │  │  ┌─────────────────────────────┐  │  │  │
│  │  │  │  Entities (Enterprise Rules) │  │  │  │
│  │  │  └─────────────────────────────┘  │  │  │
│  │  └───────────────────────────────────┘  │  │
│  └─────────────────────────────────────────┘  │
└───────────────────────────────────────────────┘

All arrows point inward →

---

The Four Layers

Entities

Enterprise-wide business rules that would exist even if there were no software.

class Order:
    def __init__(self, items: list[OrderItem]):
        self.items = items

    def total(self) -> Money:
        return sum(item.price for item in self.items)

    def apply_discount(self, rate: float) -> None:
        if rate > 0.5:
            raise ValueError("Discount cannot exceed 50%")
        for item in self.items:
            item.apply_discount(rate)

Entities have no knowledge of databases, HTTP, or frameworks. They are pure business objects.

Use Cases

Application-specific business rules. Orchestrate the flow of data to and from entities to achieve a goal.

class PlaceOrderUseCase:
    def __init__(
        self,
        order_repo: OrderRepository,   # interface, not implementation
        payment_gateway: PaymentGateway,
        notifier: Notifier,
    ):
        self.order_repo = order_repo
        self.payment_gateway = payment_gateway
        self.notifier = notifier

    def execute(self, request: PlaceOrderRequest) -> PlaceOrderResponse:
        order = Order(items=request.items)
        order.apply_discount(request.discount_rate)

        self.payment_gateway.charge(order.total())
        self.order_repo.save(order)
        self.notifier.send_confirmation(request.customer_email)

        return PlaceOrderResponse(order_id=order.id)

Use cases depend only on interfaces (ports). They do not import Flask, SQLAlchemy, or Stripe.

Interface Adapters

Convert data between the format used by use cases/entities and the format used by external agencies (HTTP, database, UI).

# Controller — converts HTTP request to use case input
class PlaceOrderController:
    def __init__(self, use_case: PlaceOrderUseCase):
        self.use_case = use_case

    def handle(self, http_request: HttpRequest) -> HttpResponse:
        request = PlaceOrderRequest(
            items=http_request.json["items"],
            discount_rate=http_request.json.get("discount", 0),
            customer_email=http_request.json["email"],
        )
        response = self.use_case.execute(request)
        return HttpResponse(status=201, body={"order_id": response.order_id})


# Gateway — converts use case calls to database operations
class SqlOrderRepository(OrderRepository):
    def save(self, order: Order) -> None:
        record = OrderRecord(id=order.id, total=float(order.total()))
        db.session.add(record)
        db.session.commit()

Frameworks & Drivers

The outermost layer: web frameworks, ORMs, databases, UI, external APIs. These are details — they plug into the system but do not shape it.

# main.py — composition root: wire everything together
from flask import Flask
from app.use_cases import PlaceOrderUseCase
from app.adapters import PlaceOrderController, SqlOrderRepository
from app.gateways import StripePaymentGateway, EmailNotifier

app = Flask(__name__)
use_case = PlaceOrderUseCase(
    order_repo=SqlOrderRepository(),
    payment_gateway=StripePaymentGateway(),
    notifier=EmailNotifier(),
)
controller = PlaceOrderController(use_case)

@app.post("/orders")
def place_order():
    return controller.handle(request)

---

Screaming Architecture

The top-level structure of a codebase should scream what the system does, not what framework it uses.

❌ Framework-screaming (what tool, not what domain):
src/
  controllers/
  models/
  views/
  migrations/

✅ Domain-screaming (what the system does):
src/
  orders/
  billing/
  inventory/
  notifications/

If someone opens the project and sees "FastAPI" or "Django" before they see "orders" or "billing", the architecture is framework-centric. The business domain should be front and center.

---

SOLID Principles in Architecture

Martin uses SOLID as the bridge between code-level design and component-level architecture.

Single Responsibility Principle

> A module should be responsible to one, and only one, actor (group of stakeholders).

# ❌ Serves two actors: Finance (calculates pay) and HR (reports hours)
class Employee:
    def calculate_pay(self): ...    # Finance owns this
    def report_hours(self): ...     # HR owns this
    def save(self): ...             # DBA owns this

# ✅ Separated by actor
class PayCalculator:
    def calculate(self, employee): ...

class HourReporter:
    def report(self, employee): ...

class EmployeeRepository:
    def save(self, employee): ...

Open/Closed Principle

> Behavior should be extensible without modification. The architecture controls what is easy to extend.

Achieved by organizing components so that high-value, stable components are protected from change — new behavior is added by extending, not editing.

Liskov Substitution Principle

> Subtypes must be substitutable for their base types without breaking the system.

In architecture, this applies to services: if a component expects an OrderRepository, any implementation of OrderRepository must be substitutable without changing the caller.

Interface Segregation Principle

> Don't depend on things you don't use.

# ❌ Fat interface forces unnecessary dependencies
class OrderRepository:
    def save(self, order): ...
    def delete(self, order_id): ...
    def generate_report(self): ...  # unrelated to most callers

# ✅ Focused interfaces
class OrderWriter:
    def save(self, order): ...

class OrderDeleter:
    def delete(self, order_id): ...

class OrderReporter:
    def generate_report(self): ...

Dependency Inversion Principle

The architectural cornerstone. High-level policy must not depend on low-level details.

# ❌ Use case directly depends on a concrete implementation
class PlaceOrderUseCase:
    def execute(self, request):
        db = PostgreSQLDatabase()   # high-level depends on detail
        db.save(...)

# ✅ Use case depends on an abstraction (interface/port)
class PlaceOrderUseCase:
    def __init__(self, order_repo: OrderRepository):  # interface
        self.order_repo = order_repo

    def execute(self, request):
        self.order_repo.save(...)   # calls interface, not impl

The dependency arrow is inverted: PostgreSQLDatabase depends on OrderRepository (implements it), not the other way around.

---

Component Cohesion Principles

These three principles decide what belongs in a component together.

Reuse/Release Equivalence Principle (REP)

> Things that are released together should be grouped together.

A component is a unit of release. All classes inside it should be cohesive enough that users would want them together in a dependency.

Common Closure Principle (CCP)

> Group things that change for the same reason at the same time.

This is SRP applied to components. If a change touches 10 files in 5 components, the architecture is working against the team. If it touches 10 files in 1 component, the architecture absorbs change cleanly.

Common Reuse Principle (CRP)

> Don't force users to depend on things they don't use.

When a component is split, things that are not used together should not be packaged together — every unnecessary dependency is a recompile and redeploy trigger.

The Tension Triangle:

       REP
      (group for reuse)
       /         \
      /           \
    CCP --------- CRP
(group by change) (split by use)

Too far toward CCP+REP → too many unrelated things together
Too far toward CCP+CRP → too many components, painful assembly
Too far toward REP+CRP → too many small packages, hard to change cohesively

---

Component Coupling Principles

These three principles control dependencies between components.

Acyclic Dependencies Principle (ADP)

> There must be no cycles in the component dependency graph.

A cycle means a group of components that must always be released together — defeating the purpose of separate components.

❌ Cycle:
  OrderService → UserService → BillingService → OrderService

✅ Break the cycle with a new component or by inverting a dependency:
  OrderService → UserService → BillingService
  OrderService → BillingInterfaces ← BillingService

Stable Dependencies Principle (SDP)

> Depend in the direction of stability.

A stable component is one that is hard to change (many things depend on it). An unstable component changes often.

Instability = efferent coupling / (afferent + efferent coupling)
  0 = maximally stable (many things depend on it, depends on nothing)
  1 = maximally unstable (depends on many, nothing depends on it)

Rule: a component's instability must be ≥ the instability of components it depends on

Do not make a stable component depend on an unstable one — a change in the unstable component forces changes in the stable one.

Stable Abstractions Principle (SAP)

> A component should be as abstract as it is stable.

Stable components that many things depend on should be abstract (interfaces, abstract classes) so they can be extended without modification.

Abstract + Stable = ideal (interfaces that many depend on)
Concrete + Unstable = ideal (leaf implementations that change often)
Concrete + Stable = "Zone of Pain" (hard to change, concrete — a problem)
Abstract + Unstable = "Zone of Uselessness" (abstract, but nobody depends on it)

---

The Humble Object Pattern

Separates testable logic from hard-to-test behavior by putting the hard part behind an interface.

Hard to test: database writes, HTTP responses, UI rendering
Easy to test: formatting, calculations, transformations

Split:
  Humble Object  → the hard part (minimal logic, just calls the hard thing)
  Testable Object → all the logic, takes and returns simple data structures

Applied to a presenter:

# Testable: pure logic, no UI dependency
class OrderPresenter:
    def format(self, order: Order) -> OrderViewModel:
        return OrderViewModel(
            id=str(order.id),
            total=f"Rp {order.total():,.0f}",
            status=order.status.display_name,
        )

# Humble: just passes the view model to the actual UI renderer
class OrderView:
    def render(self, view_model: OrderViewModel) -> HttpResponse:
        return render_template("order.html", data=view_model)

The OrderPresenter is fully testable without a web server or database. The OrderView is so simple it barely needs testing.

---

The Database Is a Detail

The database is not the center of the application. It is a plugin.

❌ Database-centric:
  Business rules call ORM models directly
  Entities are SQLAlchemy/Django models
  Schema changes ripple into business logic

✅ Database as a detail:
  Business rules depend on repository interfaces
  SQLAlchemy is only mentioned in the outermost layer
  Schema changes stay inside the adapter layer

Martin's rule: if you can swap your database from PostgreSQL to MongoDB (or an in-memory store for tests) by only changing one adapter class, the database is properly treated as a detail.

---

The Web Is a Detail

HTTP is a delivery mechanism, not the application.

❌ Web-centric:
  Use cases import Flask/Django
  Business rules depend on HttpRequest objects
  Testing requires starting a web server

✅ Web as a detail:
  Use cases accept plain Python objects (request DTOs)
  Controllers convert HttpRequest → DTO → use case
  Use cases are tested without any HTTP infrastructure

The same use case should be callable from an HTTP endpoint, a CLI command, a background job, or a test — without modification.

---

Folder Structure Example

src/
  domain/                    ← Entities (innermost)
    order.py
    payment.py

  use_cases/                 ← Use Cases
    place_order.py
    cancel_order.py
    ports/                   ← Interfaces (abstract repos, gateways)
      order_repository.py
      payment_gateway.py

  adapters/                  ← Interface Adapters
    http/
      order_controller.py
    persistence/
      sql_order_repository.py
    messaging/
      kafka_event_publisher.py

  infrastructure/            ← Frameworks & Drivers (outermost)
    database.py
    flask_app.py
    main.py                  ← Composition root

Dependency direction: infrastructureadaptersuse_casesdomain. Nothing flows outward.

---

Key Takeaways

PrincipleRule
Dependency RuleSource code dependencies point inward only
EntitiesPure business rules — no framework imports
Use CasesApplication logic — depend on interfaces, not implementations
Interface AdaptersConvert between domain types and external formats
Screaming ArchitectureTop-level structure names the domain, not the framework
Database is a detailSwap persistence by changing one adapter
Web is a detailUse cases are callable without HTTP
Humble ObjectSeparate testable logic from hard-to-test infrastructure
ADPNo cycles in component dependencies
SDP + SAPStable components must be abstract
---

Resources

  • Clean Architecture — Robert C. Martin
  • Clean Code — Robert C. Martin
  • The Clean Architecture (blog post) — Uncle Bob
  • Topics

    Clean ArchitectureSoftware ArchitectureSoftware DesignEngineering

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.