Clean Architecture
Also known as Onion Architecture and Hexagonal Architecture (Ports & Adapters) — three names for the same family of ideas, each with slightly different metaphors but the same core principle:
The dependency rule — dependencies always point inward. Inner layers know nothing about outer layers.
The domain (business logic) has zero dependencies on frameworks, databases, or UI. This makes the core of the application portable, testable, and stable regardless of infrastructure choices.
Layers
| Layer | Responsibility | Depends on |
|---|---|---|
| Domain | Entities, repository interfaces, domain exceptions | Nothing |
| Application | Use cases — orchestrate domain objects, coordinate flow | Domain |
| Infrastructure | Repository implementations, database, external services | Domain |
| UI / API | Controllers, HTTP handling, DI wiring | Application |
Domain has no outward dependencies — it's the stable core everything else builds on. UI/API is the only layer that knows the full picture, which is why DI bindings are registered there.
Domain Rules
Business rules live in the domain entities. An entity is responsible for keeping itself in a valid state at all times — its constructor validates that it's born valid, and its methods enforce invariants before mutating state.
The entity's internal state can only be changed through its own methods, not by setting properties directly from outside. Any caller gets the rules applied automatically, with no risk of forgetting a check.
Use Cases
The Application layer is organized around use cases — one class, one operation. This is an evolution from the common pattern where a UserService accumulates unrelated methods over time:
UserService.createUser() different dependencies
UserService.suspendUser() → different validations → bundled in the same class
UserService.resetPassword() different reasons to change
Instead, each operation becomes its own class: CreateUserUseCase, SuspendUserUseCase, ResetPasswordUseCase — each with a single execute() method.
Each use case:
- Has one responsibility and one reason to change
- Declares only the dependencies it actually needs
- Is easy to test in isolation
- Is the transaction boundary — one business operation = one transaction
The use case orchestrates — it calls the repository and coordinates the flow. Business rules belong in the domain, not here.
Response Objects
Use cases should not return domain entities directly. Instead, they return a dedicated response object (a DTO or record) that exposes only what the caller needs. This prevents the domain model from leaking into outer layers and makes it easier to change one without affecting the other.
The dependency rule is enforced in practice through the Dependency Inversion Principle: the Domain defines interfaces, Infrastructure implements them. See SOLID.
When to Use It
| Scenario | Approach |
|---|---|
| Small project, few operations per entity | Simple service (UserService) |
| Services growing beyond ~5-6 methods | Split into use cases |
| Multiple devs on the same codebase | Use cases avoid merge conflicts on fat services |
The signal is when a service starts having methods with completely different dependencies or concerns — not a strict line count.