Skip to main content

🧹 Clean Architecture

Layers with dependency rules

The Onion Analogy

An onion has layers:

LayerProperty
Core (center)Most important, protected
Outer layersWrap around and protect the core
Peel outer layersCore is unaffected
Core doesn't knowWhat outer layers even exist

Clean architecture is an onion. Business logic at the center, everything else (databases, web frameworks, UI) in outer layers.


What Is Clean Architecture?

A way of organizing code into layers with specific rules:

ConceptDescription
Inner layersBusiness logic (domain)
Outer layersTechnical details (database, UI, frameworks)
Key ruleInner layers CANNOT depend on outer layers
DependenciesPoint INWARD only

This keeps your most important code (business rules) protected from changes in less important code (databases, frameworks).


The Layers

           ┌─────────────────────────────────────┐
           │      Frameworks & Drivers            │
           │   (Web, Database, UI, External)      │
           │ ┌───────────────────────────────┐   │
           │ │     Interface Adapters        │   │
           │ │  (Controllers, Presenters)    │   │
           │ │ ┌───────────────────────────┐ │   │
           │ │ │   Application Layer       │ │   │
           │ │ │      (Use Cases)          │ │   │
           │ │ │ ┌───────────────────────┐ │ │   │
           │ │ │ │     Entities          │ │ │   │
           │ │ │ │   (Domain/Core)       │ │ │   │
           │ │ │ └───────────────────────┘ │ │   │
           │ │ └───────────────────────────┘ │   │
           │ └───────────────────────────────┘   │
           └─────────────────────────────────────┘

           Dependencies should point INWARD ←

Layer Details

1. Entities (Domain/Core) - Innermost

The purest business logic. Rules that would exist even without software.

ContainsExamples
Business objectsUser, Order, Payment
Business rules"Account balance cannot be negative"
Domain logic"Orders must have at least one item"

No database. No HTTP. No frameworks. Just pure business rules.

2. Use Cases (Application Layer)

Application-specific business rules. Orchestrates the flow of data.

ResponsibilityExample
Orchestrate operationsPlaceOrder use case
Call domain logicValidate order
Use interfacesSave order (via interface)
Coordinate flowCalculate total, send confirmation

Knows WHAT needs to happen. Doesn't know HOW (database, email provider).

3. Interface Adapters

Converts data between layers.

ComponentPurpose
ControllersHTTP request → Use case input
PresentersUse case output → HTTP response
Repository implementationsDatabase → Domain entities

Translates between external formats and internal domain.

4. Frameworks & Drivers - Outermost

All the technical stuff that can change.

ExamplesWhy It's Outer
Web framework (Express, Django)Could swap frameworks
Database (PostgreSQL, MongoDB)Could change databases
External services (Stripe, SendGrid)Could change providers
UI (React, Vue)Could change frontend

Details that can change without affecting business logic.


The Dependency Rule

THE KEY PRINCIPLE: Source code dependencies can only point INWARD.

LayerWhat It Knows About
EntitiesNothing (pure domain)
Use CasesEntities only
AdaptersUse cases and entities
FrameworksEverything (outer can know inner)

Why This Matters

Change This...Affects...
DatabaseOnly outer layer, business logic unchanged
Web frameworkOnly outer layer, use cases unchanged
UIOnly outer layer, core logic unchanged

Core business logic is protected from change.


Crossing Boundaries with Interfaces

Problem: Use case needs to save data. But use case is inner, database is outer. Use case can't import database!

Solution: Define interface in inner layer.

Inner layer (Use Cases):
  interface UserRepository:
    save(user)
    findById(id)

Outer layer (Frameworks):
  class PostgresUserRepository implements UserRepository:
    save(user) → INSERT INTO users...

Use case uses interface.
Outer layer provides implementation.
Dependency still points inward!

This is called Dependency Inversion.


Project Structure Example

src/
├── domain/            ← Entities (innermost)
│   ├── user.py
│   └── order.py
│
├── application/       ← Use Cases
│   ├── place_order.py
│   └── interfaces/    ← Ports (what we need)
│       └── user_repository.py
│
├── adapters/          ← Interface Adapters
│   ├── controllers/
│   │   └── order_controller.py
│   └── repositories/
│       └── postgres_user_repository.py
│
└── frameworks/        ← Frameworks & Drivers (outermost)
    ├── web/
    │   └── express_app.py
    └── database/
        └── postgres_connection.py

Benefits

BenefitHow It Works
Framework independenceFramework is a detail, can swap
TestableTest domain without database, mock outer layers
UI independenceSame logic: web, mobile, CLI, API
Database independencePostgreSQL today, MongoDB tomorrow
Long-term maintainabilityBusiness logic protected from tech changes

Clean Architecture vs Others

ArchitectureCreatorCore Concept
Clean ArchitectureRobert "Uncle Bob" MartinLayers with dependency rule
Hexagonal ArchitectureAlistair CockburnPorts and adapters
Onion ArchitectureJeffrey PalermoDomain at center

All share: Domain at center, dependencies pointing inward. Different metaphors, same principles.


Common Mistakes

1. Entities Know About Persistence

❌ Bad✅ Good
@Column("name") decorator in EntityPure name: str
ORM annotations in domainPersistence in adapter layer

2. Use Cases Know About HTTP

❌ Bad✅ Good
def place_order(request: HttpRequest)def place_order(user_id, items)
HTTP in use caseController handles HTTP

3. Skipping Layers

❌ Bad✅ Good
Controller → Repository directlyController → Use Case → Repository

Use cases contain business logic. Don't bypass them.


FAQ

Q: Isn't this overengineering?

For simple CRUD apps? Perhaps. For complex domains with long lifespan, the investment pays off in maintainability.

Q: How do I start with existing code?

Gradually. Extract domain first. Add interfaces. Move outward over time.

Q: What about performance?

Layers add some indirection. Rarely significant. Optimize only if measured.

Q: Does this work with microservices?

Yes! Each microservice can have clean architecture internally.


Summary

Clean architecture organizes code in layers where dependencies point inward, protecting business logic from external concerns.

Key Takeaways:

  • Entities at center (pure business rules)
  • Use cases orchestrate application logic
  • Adapters convert between layers
  • Frameworks at the edge
  • Dependencies point inward ONLY
  • Interfaces enable dependency inversion
  • Business logic protected from change

The result: code that's testable, flexible, and maintainable for years!

Leave a Comment

Comments (0)

Be the first to comment on this concept.

Comments are approved automatically.