The Onion Analogy
An onion has layers:
| Layer | Property |
|---|---|
| Core (center) | Most important, protected |
| Outer layers | Wrap around and protect the core |
| Peel outer layers | Core is unaffected |
| Core doesn't know | What 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:
| Concept | Description |
|---|---|
| Inner layers | Business logic (domain) |
| Outer layers | Technical details (database, UI, frameworks) |
| Key rule | Inner layers CANNOT depend on outer layers |
| Dependencies | Point 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.
| Contains | Examples |
|---|---|
| Business objects | User, 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.
| Responsibility | Example |
|---|---|
| Orchestrate operations | PlaceOrder use case |
| Call domain logic | Validate order |
| Use interfaces | Save order (via interface) |
| Coordinate flow | Calculate total, send confirmation |
Knows WHAT needs to happen. Doesn't know HOW (database, email provider).
3. Interface Adapters
Converts data between layers.
| Component | Purpose |
|---|---|
| Controllers | HTTP request → Use case input |
| Presenters | Use case output → HTTP response |
| Repository implementations | Database → Domain entities |
Translates between external formats and internal domain.
4. Frameworks & Drivers - Outermost
All the technical stuff that can change.
| Examples | Why 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.
| Layer | What It Knows About |
|---|---|
| Entities | Nothing (pure domain) |
| Use Cases | Entities only |
| Adapters | Use cases and entities |
| Frameworks | Everything (outer can know inner) |
Why This Matters
| Change This... | Affects... |
|---|---|
| Database | Only outer layer, business logic unchanged |
| Web framework | Only outer layer, use cases unchanged |
| UI | Only 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
| Benefit | How It Works |
|---|---|
| Framework independence | Framework is a detail, can swap |
| Testable | Test domain without database, mock outer layers |
| UI independence | Same logic: web, mobile, CLI, API |
| Database independence | PostgreSQL today, MongoDB tomorrow |
| Long-term maintainability | Business logic protected from tech changes |
Clean Architecture vs Others
| Architecture | Creator | Core Concept |
|---|---|---|
| Clean Architecture | Robert "Uncle Bob" Martin | Layers with dependency rule |
| Hexagonal Architecture | Alistair Cockburn | Ports and adapters |
| Onion Architecture | Jeffrey Palermo | Domain 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 Entity | Pure name: str |
| ORM annotations in domain | Persistence 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 case | Controller handles HTTP |
3. Skipping Layers
| ❌ Bad | ✅ Good |
|---|---|
| Controller → Repository directly | Controller → 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!
Related Concepts
Leave a Comment
Comments (0)
Be the first to comment on this concept.
Comments are approved automatically.