Home / Notebooks / Systems & Architecture
Systems & Architecture
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: infrastructure → adapters → use_cases → domain. 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.