Clean Architecture
Organize code in concentric layers where dependencies always point inward.
Clean Architecture: A Complete Guide with Examples in 11 Programming Languages
Clean Architecture is a software design philosophy introduced by Robert C. Martin (Uncle Bob) that organizes code into concentric layers — each with a clear responsibility and a strict dependency rule: source code dependencies can only point inward. The innermost layer contains the core business logic; the outermost layers contain frameworks, databases, and UI. Nothing in an inner layer knows anything about an outer layer. The result is a system where the business rules are completely isolated from infrastructure, making them independently testable, independently deployable, and resilient to technology changes.
In this comprehensive guide, we'll explore Clean Architecture with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and the judgment to apply it where it genuinely fits.
Table of Contents
- What is Clean Architecture?
- Why Use Clean Architecture?
- Clean Architecture Comparison
- Clean Architecture Explained
- Architecture Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is Clean Architecture?
Most software starts simple: a few routes, a database, and some business logic glued together. Over time, the database leaks into the business logic; the framework owns the application; changing the ORM requires rewriting half the codebase. The architecture has inverted — instead of the business rules owning the system, the infrastructure owns the business rules.
Clean Architecture solves this by enforcing a single rule with profound consequences: dependencies point inward only. Business entities know nothing about use cases. Use cases know nothing about controllers. Controllers know nothing about frameworks. Each layer depends only on the layer inside it — never on anything outside.
The architecture is organized into four concentric layers:
-
Entities (innermost): Enterprise-wide business rules. Pure domain objects that model the core concepts of the business —
Order,User,Invoice. They have no dependencies on databases, frameworks, or UI. They are the most stable part of the system and change only when business rules change. -
Use Cases: Application-specific business rules. Each use case represents one thing the system does —
PlaceOrder,RegisterUser,GenerateInvoice. Use cases orchestrate the flow of data between entities and the outside world, calling domain logic and coordinating data access through abstractions (interfaces). They depend only on Entities. -
Interface Adapters: Convert data between the format most convenient for use cases and the format most convenient for external agents (UI, databases, services). Controllers, Presenters, Gateways, and Repository implementations live here. They depend on Use Cases.
-
Frameworks & Drivers (outermost): The glue layer. Web frameworks, database drivers, UI libraries, external APIs. This layer depends on everything inside it, but nothing inside depends on it. It is replaceable.
The Dependency Rule is absolute: source code dependencies must always point inward. An entity never imports a use case. A use case never imports a controller. A controller never imports a framework directly — it imports an interface defined in the use case layer, and the framework layer provides the implementation. This inversion of control is what decouples business logic from infrastructure.
Why Use Clean Architecture?
Clean Architecture is justified when one or more of these requirements are present:
-
Testable business logic: Because use cases depend on interfaces — not concrete databases, frameworks, or HTTP clients — every business rule can be tested with simple in-memory fakes. No database setup, no HTTP server, no file system. Tests run in milliseconds.
-
Technology independence: The business logic does not know or care whether data is stored in PostgreSQL, MongoDB, or an in-memory dictionary. Swapping the database means implementing a new Repository class in the outer layer — the use cases and entities are untouched.
-
Framework independence: The application does not depend on Django, Spring, Express, or Laravel. Frameworks are details — plugins to the application. If the framework is upgraded, deprecated, or replaced, the business logic survives unchanged.
-
UI independence: The same use cases can serve a REST API, a GraphQL endpoint, a CLI, or a background worker. The presentation layer changes; the business rules do not.
-
Long-term maintainability: Clear layer boundaries enforce separation of concerns at the architectural level. New developers understand where code belongs. Violations of the dependency rule produce visible, compiler-catchable import errors (in typed languages) rather than subtle coupling.
-
Scalability of the team: Teams can own individual layers independently. The database team owns repositories; the domain team owns entities and use cases; the API team owns controllers. The interfaces between them are the contracts.
Clean Architecture Comparison
| Architecture | Layer Separation | Dependency Direction | Framework Coupling | Testability | Use When |
|---|---|---|---|---|---|
| Clean Architecture | Strict (4 layers, dependency rule) | Always inward | None — framework is a plugin | Excellent | Complex domain, long-lived systems, multi-team development |
| Layered (N-Tier) | Horizontal (Presentation → Business → Data) | Top-down | Moderate | Moderate | Standard enterprise apps, familiar team structure |
| Hexagonal (Ports & Adapters) | Inside/Outside (core vs adapters) | Inward (same rule) | None | Excellent | Event-driven systems, multiple delivery mechanisms |
| MVC | Model/View/Controller | Cyclic (often) | High (framework-driven) | Low-Moderate | Web UIs, rapid prototyping, framework-first apps |
| Transaction Script | None (per-procedure) | None | High | Low | Simple CRUD, scripting-style backends |
Clean Architecture vs. Hexagonal (Ports & Adapters): These are philosophically identical — both enforce inward dependencies and use interfaces to decouple the core from infrastructure. Clean Architecture adds explicit naming for layers (Entities, Use Cases, Interface Adapters, Frameworks) and distinguishes enterprise rules (Entities) from application rules (Use Cases). Hexagonal Architecture uses "ports" (interfaces in the core) and "adapters" (implementations in the outside). They are complementary; many practitioners use both terms interchangeably.
Clean Architecture vs. Layered Architecture: Layered architecture (Presentation → Business Logic → Data Access) looks similar but has a critical flaw: the dependency direction is top-down, meaning Business Logic typically depends on the Data Access layer. In Clean Architecture, the dependency is inverted — Data Access depends on a repository interface defined in the Use Case layer. This inversion is what makes Clean Architecture testable and technology-agnostic.
Clean Architecture vs. MVC: MVC is a UI pattern, not an application architecture. It organizes the presentation layer. Clean Architecture organizes the entire system, including everything MVC doesn't address: how business rules relate to the database, how use cases relate to entities, how the application relates to the framework. Clean Architecture can contain MVC at its outer layer.
Clean Architecture Explained
Core Components
Entity: A domain object that encapsulates enterprise-wide business rules. An Order entity knows that it cannot be shipped without a confirmed payment. A User entity knows that an email address must be unique per account. Entities are plain objects — no ORMs, no HTTP, no framework decorators. They are the most stable code in the system.
Use Case (Interactor): An application-specific business rule. Each use case has a single responsibility: PlaceOrderUseCase orchestrates the act of placing an order — validating the cart, reserving inventory, creating the order entity, persisting it through a repository interface, and dispatching a domain event. Use cases are the entry point to business logic.
Repository Interface (Port): An abstraction defined in the Use Case layer that describes what data operations the use case needs — without specifying how they are implemented. IOrderRepository declares find_by_id(id) and save(order). The use case depends on this interface. The concrete implementation (hitting PostgreSQL, MongoDB, or an in-memory dict) lives in the outer layer.
Controller: Sits in the Interface Adapters layer. Receives input from the delivery mechanism (HTTP request, CLI argument, message queue event), transforms it into the format the use case expects (a request model or DTO), calls the use case, receives the output model, and transforms it into the format the delivery mechanism expects (HTTP response, JSON). Controllers do not contain business logic.
Presenter: Transforms the use case output model into the view model that the UI or API response requires. In REST APIs, the Presenter often merges into the Controller. In more complex applications (rich UIs, multiple output formats), they are kept separate.
Gateway / Repository Implementation: The concrete implementation of a repository interface. Lives in the Frameworks & Drivers layer (or Interface Adapters, depending on convention). Knows how to talk to a specific database using a specific ORM or query builder.
Request / Response Models (DTOs): Simple data structures that cross layer boundaries. The use case defines its own input and output models — not the HTTP request object, not the ORM model. This prevents framework types from leaking into business logic.
The Dependency Rule in Practice
When a Use Case needs to save an Order, it does not call db.save(order). Instead:
- The Use Case layer defines
IOrderRepositorywith asave(order: Order)method. - The Use Case depends on
IOrderRepository(an interface — inward pointing). - The Frameworks & Drivers layer defines
PostgresOrderRepository(IOrderRepository)which implementssave()using the actual ORM. - At runtime, a dependency injection container provides the concrete
PostgresOrderRepositorywhereverIOrderRepositoryis needed.
The Use Case never knows which database is being used. The Entity never knows a Use Case exists. The database never appears in business logic. This is the Dependency Rule in action.
Clean Architecture Variants
- Screaming Architecture: The top-level package structure names domain concepts (
orders/,users/,billing/), not technical layers (controllers/,repositories/,services/). The architecture "screams" the system's purpose, not its framework. - Feature Slices: Each vertical slice contains all layers for one feature (entity, use case, controller, repository) in one module. Reduces cross-feature coupling; popular in large teams with feature ownership.
- Modular Monolith: Clean Architecture applied within a single deployable, with strict module boundaries. Enables future decomposition into microservices by making bounded contexts explicit.
- Microservices: Each service owns its own Clean Architecture stack. Inter-service communication happens at the Frameworks & Drivers layer (HTTP clients, message bus adapters) through use-case-level interface contracts.
Architecture Diagram
Here's the layer diagram showing Clean Architecture structure:
Beginner-Friendly Example
Let's start with the most intuitive Clean Architecture example: a task management system. A user submits a request to create a task. The request goes through a Controller, which calls a CreateTaskUseCase, which validates the task, creates a Task entity, persists it through an ITaskRepository interface, and returns a response. The database implementation lives in the outer layer and can be swapped without touching the use case.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol
from uuid import UUID, uuid4
from datetime import datetime
# ── ENTITY (innermost layer) ─────────────────────────────────────────────────
@dataclass
class Task:
id: UUID
title: str
completed: bool = False
created_at: datetime = field(default_factory=datetime.utcnow)
def complete(self) -> None:
if self.completed:
raise ValueError("Task is already completed")
self.completed = True
@staticmethod
def create(title: str) -> "Task":
if not title.strip():
raise ValueError("Task title cannot be empty")
return Task(id=uuid4(), title=title.strip())
# ── USE CASE LAYER ───────────────────────────────────────────────────────────
@dataclass
class CreateTaskRequest:
title: str
@dataclass
class CreateTaskResponse:
id: str
title: str
completed: bool
class ITaskRepository(Protocol):
def save(self, task: Task) -> None: ...
def find_by_id(self, task_id: UUID) -> Task | None: ...
class CreateTaskUseCase:
def __init__(self, repository: ITaskRepository) -> None:
self._repository = repository
def execute(self, request: CreateTaskRequest) -> CreateTaskResponse:
task = Task.create(request.title)
self._repository.save(task)
return CreateTaskResponse(
id=str(task.id),
title=task.title,
completed=task.completed,
)
# ── INTERFACE ADAPTERS ───────────────────────────────────────────────────────
class TaskController:
def __init__(self, use_case: CreateTaskUseCase) -> None:
self._use_case = use_case
def create(self, http_body: dict) -> dict:
request = CreateTaskRequest(title=http_body.get("title", ""))
try:
response = self._use_case.execute(request)
return {"status": 201, "data": {"id": response.id, "title": response.title}}
except ValueError as e:
return {"status": 400, "error": str(e)}
# ── FRAMEWORKS & DRIVERS (outermost layer) ───────────────────────────────────
class InMemoryTaskRepository:
def __init__(self) -> None:
self._store: dict[UUID, Task] = {}
def save(self, task: Task) -> None:
self._store[task.id] = task
def find_by_id(self, task_id: UUID) -> Task | None:
return self._store.get(task_id)
# ── COMPOSITION ROOT ─────────────────────────────────────────────────────────
if __name__ == "__main__":
repository = InMemoryTaskRepository()
use_case = CreateTaskUseCase(repository)
controller = TaskController(use_case)
result = controller.create({"title": "Learn Clean Architecture"})
print(result)
# {'status': 201, 'data': {'id': '...', 'title': 'Learn Clean Architecture'}}
Production-Ready Example
The beginner example shows the structure. The production example shows the reality: a user registration system with email uniqueness validation, password hashing (delegated to a port), domain events for post-registration notifications, and full error handling across all 11 languages.
The use case orchestrates: validate the request, check email uniqueness through a repository port, hash the password through a hasher port, create the User entity, persist it, dispatch a UserRegisteredEvent, and return a response. The use case depends on three interfaces — IUserRepository, IPasswordHasher, and IEventBus — all defined in the use case layer, all implemented in the outer layers.
🐍 Python (Production)
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol
from uuid import UUID, uuid4
from datetime import datetime
import hashlib
import re
# ── ENTITIES ─────────────────────────────────────────────────────────────────
@dataclass
class User:
id: UUID
email: str
password_hash: str
registered_at: datetime = field(default_factory=datetime.utcnow)
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+quot;)
@staticmethod
def create(email: str, password_hash: str) -> "User":
email = email.strip().lower()
if not User.EMAIL_PATTERN.match(email):
raise ValueError(f"Invalid email address: {email}")
if not password_hash:
raise ValueError("Password hash cannot be empty")
return User(id=uuid4(), email=email, password_hash=password_hash)
@dataclass(frozen=True)
class UserRegisteredEvent:
user_id: UUID
email: str
occurred_at: datetime = field(default_factory=datetime.utcnow)
# ── USE CASE PORTS (Interfaces) ───────────────────────────────────────────────
class IUserRepository(Protocol):
def save(self, user: User) -> None: ...
def find_by_email(self, email: str) -> User | None: ...
class IPasswordHasher(Protocol):
def hash(self, plain_password: str) -> str: ...
def verify(self, plain_password: str, hashed: str) -> bool: ...
class IEventBus(Protocol):
def publish(self, event: object) -> None: ...
# ── REQUEST / RESPONSE MODELS ─────────────────────────────────────────────────
@dataclass
class RegisterUserRequest:
email: str
password: str
@dataclass
class RegisterUserResponse:
user_id: str
email: str
# ── USE CASE ──────────────────────────────────────────────────────────────────
class RegisterUserUseCase:
def __init__(
self,
user_repo: IUserRepository,
password_hasher: IPasswordHasher,
event_bus: IEventBus,
) -> None:
self._user_repo = user_repo
self._password_hasher = password_hasher
self._event_bus = event_bus
def execute(self, request: RegisterUserRequest) -> RegisterUserResponse:
if not request.password or len(request.password) < 8:
raise ValueError("Password must be at least 8 characters")
existing = self._user_repo.find_by_email(request.email)
if existing:
raise ValueError(f"Email '{request.email}' is already registered")
password_hash = self._password_hasher.hash(request.password)
user = User.create(email=request.email, password_hash=password_hash)
self._user_repo.save(user)
self._event_bus.publish(UserRegisteredEvent(user_id=user.id, email=user.email))
return RegisterUserResponse(user_id=str(user.id), email=user.email)
# ── FRAMEWORK ADAPTERS ────────────────────────────────────────────────────────
class InMemoryUserRepository:
def __init__(self) -> None:
self._store: dict[UUID, User] = {}
def save(self, user: User) -> None:
self._store[user.id] = user
def find_by_email(self, email: str) -> User | None:
return next((u for u in self._store.values() if u.email == email.lower()), None)
class Sha256PasswordHasher:
def hash(self, plain_password: str) -> str:
return hashlib.sha256(plain_password.encode()).hexdigest()
def verify(self, plain_password: str, hashed: str) -> bool:
return self.hash(plain_password) == hashed
class ConsoleEventBus:
def publish(self, event: object) -> None:
print(f"[EVENT] {type(event).__name__}: {event}")
# ── COMPOSITION ROOT ──────────────────────────────────────────────────────────
if __name__ == "__main__":
repo = InMemoryUserRepository()
hasher = Sha256PasswordHasher()
event_bus = ConsoleEventBus()
use_case = RegisterUserUseCase(repo, hasher, event_bus)
response = use_case.execute(RegisterUserRequest(
email="alice@example.com",
password="securepassword123"
))
print(f"Registered: {response.user_id} ({response.email})")
# [EVENT] UserRegisteredEvent: ...
# Registered: <uuid> (alice@example.com)
Real-World Use Cases
Clean Architecture genuinely improves long-term outcomes in these scenarios:
E-Commerce Platforms: Order placement, payment processing, inventory management — each domain action is a distinct use case. The payment gateway, the inventory service, and the notification service are all infrastructure details behind repository and service interfaces. Swapping Stripe for PayPal means writing one new adapter class.
Healthcare Systems: Patient records, appointment scheduling, prescription management. The strict layer separation makes it straightforward to audit every operation (business rules enforce audit logging in the use case layer), comply with regulations, and switch between different data storage requirements without touching clinical logic.
Banking and Financial Services: Transaction processing, account management, ledger operations. Clean Architecture ensures that the core financial rules (no overdraft without authorization, every transaction must balance) live in the entity and use case layers — never in a database trigger or a framework middleware.
Multi-Platform Applications: The same use cases serve a mobile app (via a REST API adapter), a web app (via a GraphQL adapter), and a batch processing system (via a CLI adapter). The business rules are written once and tested once.
Microservices Migration: A monolith structured with Clean Architecture is already decomposed along domain boundaries. Extracting a microservice means taking a vertical slice — entities, use cases, and interfaces for one bounded context — and deploying it independently with new framework adapters.
Language-Specific Mistakes and Anti-Patterns
- Importing framework objects in use cases:
from django.http import HttpRequestin a use case violates the dependency rule — use cases must not import Django - Using ORM models as entities: Passing
Task.objects.get(id=id)(a Django model) directly into a use case couples your business logic to the ORM; create a separate domain entity and a repository that maps between them - Missing Protocol for interfaces: Without
Protocol, Python's duck typing silently accepts wrong implementations; always defineIRepository(Protocol)so type checkers enforce the contract
Frequently Asked Questions
Q1: Is Clean Architecture overkill for a small project?
For a simple CRUD application with two or three entities, a single developer, and a short lifespan — yes, Clean Architecture adds more ceremony than value. The explicit layer boundaries, request/response models, and repository interfaces multiply the number of files for simple operations.
The pattern pays off when: the domain has meaningful business rules (not just data storage), multiple people work on the codebase, the application is expected to live for years, or technology choices might change. A practical test: if your use cases contain real validation, orchestration, and domain decisions — rather than just passing data through to the database — Clean Architecture gives those rules a safe, testable home.
Q2: Where does validation belong — in the entity, the use case, or the controller?
All three, at different levels:
- Controller/Adapter layer: Input validation — "is this a valid JSON body? Is the email field present?" — happens at the boundary before the use case is called. This prevents garbage from entering the system.
- Use case layer: Business validation — "does a user with this email already exist? Does this account have sufficient balance?" — happens in the use case. These are application-level rules.
- Entity layer: Domain invariant enforcement — "an Order cannot be shipped without payment; a Task title cannot be empty" — happens in the entity's factory method or business methods. These are the rules that must always hold, regardless of which use case triggered the change.
The hierarchy ensures that even if a use case forgets to validate, the entity protects its own invariants.
Q3: How do I handle cross-cutting concerns like logging and authentication?
Cross-cutting concerns live in the Interface Adapters layer, implemented as middleware, decorators, or interceptors that wrap use cases:
- Logging: An
LoggingUseCasedecorator wraps the real use case, logs the request and response, and delegates. The use case itself has no logging code. - Authentication: A controller or middleware verifies the JWT, extracts the authenticated user identity, and passes it as part of the request model to the use case. Authentication is a controller-layer concern.
- Authorization: Coarse-grained authorization (is the user logged in?) belongs at the controller layer. Fine-grained authorization (can this user access this resource?) often belongs in the use case, where it can apply domain rules.
Q4: How does dependency injection work in Clean Architecture?
The Composition Root — the single place in the application that wires everything together — is where all dependencies are instantiated and injected. In a web application, this is typically the startup or bootstrap file.
The Composition Root lives in the outermost layer (Frameworks & Drivers). It knows about all layers — it imports the concrete PostgresUserRepository, the BCryptPasswordHasher, and the CreateUserUseCase, and wires them together. Nothing else knows about the concrete implementations; everything else depends on interfaces.
DI containers (Spring, .NET's built-in DI, Hilt, Provider) automate this wiring but do not change the architectural principle: the dependency rule still applies; the container is just a tool in the outer layer.
Q5: What is the difference between Clean Architecture and Domain-Driven Design (DDD)?
They are complementary, not competing. DDD provides the strategic tools (bounded contexts, ubiquitous language, aggregates, domain events) for modeling complex domains. Clean Architecture provides the structural organization for implementing those models in code.
A DDD aggregate maps to a Clean Architecture entity. DDD domain events are dispatched through the use case layer via an event bus port. DDD repositories are the interfaces defined in the use case layer. DDD bounded contexts map to Clean Architecture modules with their own set of layers.
Many teams apply both: DDD for domain modeling and Clean Architecture for code organization. Neither requires the other, but they reinforce each other.
Q6: How do I handle database transactions that span multiple use cases?
Database transactions should not span multiple use case executions. Each use case execution should be one transaction — all the work of the use case either commits together or rolls back together.
If you find yourself needing a transaction across use cases, this usually indicates one of two things: either the operation should be a single, more specific use case; or you have an eventual consistency problem that should be solved with domain events and the Saga pattern rather than a distributed transaction.
For operations that must be atomic across multiple aggregates, the Unit of Work pattern coordinates the commit. The Unit of Work interface is defined in the use case layer; the implementation (wrapping a database transaction) lives in the infrastructure layer.
Key Takeaways
🎯 Core Concept: Clean Architecture organizes code into four concentric layers — Entities, Use Cases, Interface Adapters, and Frameworks & Drivers — with one absolute rule: source code dependencies point inward only. Business entities know nothing about use cases; use cases know nothing about databases or HTTP. Infrastructure depends on business logic — never the reverse. This inversion is what makes Clean Architecture independently testable, independently deployable, and resilient to technology changes. The business rules are the center of gravity; everything else is a replaceable detail.
🔑 Key Benefits:
- Testable business rules: Use cases depend on interfaces, not concrete databases; every business rule can be tested with fast, in-memory fakes — no database setup required
- Technology independence: Swapping databases, frameworks, or external services means writing new adapter classes in the outer layer — entities and use cases are untouched
- Framework independence: The application does not depend on any framework; frameworks are plugins to the outer layer, not load-bearing walls
- Screaming purpose: The architecture reflects the domain (orders, users, invoices) rather than the technology (controllers, services, repositories)
- Parallel development: Teams own layers independently; the interfaces between layers are the contracts that enable parallel work
- Long-term maintainability: New developers understand where code belongs; violations of the dependency rule are visible and compiler-catchable in typed languages
🌐 Language-Specific Highlights:
- Python: Use
Protocolfor interface definitions — structural typing means implementations don't need to inherit;@dataclassfor request/response models; type aliases for repository return types;from __future__ import annotationsfor forward references - TypeScript: Use
interfacefor ports and strict module boundaries enforced with ESLint;readonlyfor request/response DTOs;typealiases for primitive value objects; barrel files (index.ts) to control layer exports - Java:
recordfor immutable request/response models;sealed interfacefor domain error hierarchies;@Transactionalbelongs in repository implementations, not use cases; Spring's@Componentstays in the framework layer - JavaScript: Private class fields (
#field) for entity encapsulation;Object.freeze()for immutable value objects; a singlecomposition-root.jsfile for all wiring; JSDoc types for interface contracts when TypeScript is not used - C#:
interfacefor ports with theIprefix convention;recordfor immutable DTOs;IServiceCollection.AddScoped<IRepository, PostgresRepository>()in the composition root;MediatRfor use case dispatch as an optional pattern - PHP:
readonly class(PHP 8.2+) for immutable DTOs;interfacefor all ports; PHP 8'smatchexpression for clean error mapping; PSR-4 autoloading with namespace structure that mirrors the layer hierarchy - Go: Interfaces defined in the package that uses them (use case package owns the repository interface); functional options for use case configuration;
context.Contextpropagation through all use case signatures for cancellation and tracing - Rust:
traitfor ports;Box<dyn Repository>for dynamic dispatch in use cases;Arc<dyn Repository>for shared ownership in async contexts;thiserrorcrate for structured domain error types;Result<T, DomainError>for all use case returns - Dart: Abstract interface classes for ports (Dart 3); BLoC or Riverpod as the presentation layer calling use cases;
Either<Failure, Success>(fromdartzpackage) for functional error handling in use cases; Flutter-free use case layer testable with pure Dart test runner - Swift:
protocolfor ports;actorfor thread-safe repository implementations;async/awaitthrough all use case signatures;Result<T, Error>for use case returns;@MainActorin view models that call use cases - Kotlin:
interfacefor ports;sealed classfor domain error hierarchies;suspend funfor all use case signatures; coroutine scope management in the adapter layer;Result<T>or Arrow'sEither<E, A>for functional error handling
📋 The Dependency Rule:
- Entities depend on nothing — pure domain logic, zero imports from outer layers
- Use Cases depend only on Entities and port interfaces they define themselves
- Interface Adapters depend on Use Cases and Entities — never on Framework types
- Frameworks & Drivers depend on Interface Adapters — and implement the port interfaces
- Violations are always the same pattern: an inner layer importing from an outer layer — fix by introducing an interface (port) in the inner layer and implementing it in the outer layer
⚠️ Anti-Patterns to Avoid:
- Framework types (
HttpRequest,DbContext,@Entity) leaking into use cases or entities - ORM models used as domain entities — always map at the repository boundary
- Business logic in controllers — controllers transform and delegate; they never decide
- Fat service classes that handle multiple unrelated use cases — one class per use case
- Composition root logic scattered across the application — all wiring in one place
- Skipping the repository interface and calling the ORM directly from use cases
🛠️ Best Practices Across Languages:
- Start with the use case: write the use case first with placeholder interfaces; let the use case tell you what ports it needs
- Name use cases after business operations:
PlaceOrderUseCase,RegisterUserUseCase,GenerateInvoiceUseCase— notUserServiceorOrderManager - Keep entities pure: no imports from frameworks, ORMs, HTTP, or use cases — entities are timeless domain models
- One use case per class: a class that handles registration, login, and password reset is not Clean Architecture — split them
- Map at every boundary: controller maps HTTP → request model; use case maps domain → response model; repository maps ORM model → entity. Never let framework types cross layer boundaries
- Test use cases in isolation: the entire use case test suite should run without a database, HTTP server, or any external system — inject in-memory fakes for all ports
- Composition root owns all wiring: one file (or one DI container configuration) that knows all concrete implementations; everything else knows only interfaces
💡 Clean Architecture + Related Patterns:
- Clean Architecture + Repository Pattern: repositories are the primary port between use cases and data storage; use cases define the repository interfaces; implementations live in the infrastructure layer
- Clean Architecture + Unit of Work: use cases coordinate multiple repository operations within one business transaction; the Unit of Work interface is a port defined in the use case layer
- Clean Architecture + Domain Events: use cases collect domain events raised by entities; an event bus port (defined in the use case layer) dispatches them after the transaction commits
- Clean Architecture + CQRS: the write side is a use case that mutates state; the read side is a query object that fetches projections directly — no domain entities needed for reads
- Clean Architecture + Event Sourcing: entities are aggregates that apply events; the event store is a repository implementation; use cases load aggregates, execute commands, and append events through the repository port
This guide pairs directly with:
- Repository Pattern — the primary port between use cases and data storage in Clean Architecture
- Unit of Work Pattern — transactional coordination for multi-aggregate use cases
- CQRS — separating command and query responsibilities within the use case layer
- Domain-Driven Design — modeling the entities and domain events that Clean Architecture organizes
- Event Sourcing — storing state as events, with the event store as the repository implementation
This guide is part of the Design Patterns in Action series, which covers:
- Visitor Pattern — New operations on a stable hierarchy
- Memento Pattern — Capture and restore object state
- Observer Pattern — Event-driven notifications
- State Pattern — Behavior changes with lifecycle phase
- Strategy Pattern — Interchangeable algorithms
- Facade Pattern — Simplified interface to a subsystem
- MVC Pattern — Model-View-Controller architecture
- Unit of Work Pattern — Coordinate database changes atomically
- Event Sourcing — Store state as an immutable event log
- Clean Architecture — Organize code by dependency direction
Each guide covers all 11 programming languages with production-ready examples, language-specific best practices, and comprehensive anti-pattern analysis.