Skip to content

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

UI / API  ──►  Application  ──►  Domain
                          Infrastructure
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.