Software Architecture & Design Principles
Clean Architecture Boundaries
The Dependency Rule: source-code dependencies may only point inward, from mechanism toward policy. Concentric layers — Entities, Use Cases, Interface Adapters, Frameworks & Drivers — keep business rules ignorant of the database, the UI, and the framework du jour.
- Dependency Rule: nothing in an inner circle may know anything about an outer circle
- Entities = enterprise-wide business rules; Use Cases = application-specific business rules that orchestrate entities
- Interface Adapters translate between use-case-friendly data and framework/DB/UI-friendly data
- Frameworks & Drivers (web framework, database, UI toolkit) are the outermost, most volatile, most replaceable ring
- Crossing a boundary inward-to-outward uses an interface owned by the inner ring; the outer ring implements it (dependency inversion in practice)
- Data crossing boundaries should be simple structures (DTOs), never framework/ORM entities leaking into business rules
| Ring | Contains | Knows about |
|---|---|---|
| Frameworks & Drivers | Web framework, DB driver, UI, external services | Everything inward |
| Interface Adapters | Controllers, Presenters, Gateways | Use Cases' interfaces; converts to/from framework shapes |
| Use Cases | Application-specific business rules (interactors) | Entities only |
| Entities | Enterprise-wide business rules and data | Nothing outward — pure domain logic |
The Dependency Rule is the one non-negotiable constraint: "source code dependencies can only point inwards." A UserRepository interface lives with the use case that needs it (inner ring); a SqlUserRepository implementation lives in the outer ring and implements that interface. The use case never imports anything from the persistence framework — it depends on an abstraction it owns, and the database code depends on that abstraction too, just from the other side. This is Dependency Inversion And Injection applied at the architectural seam, and it is what makes the business rules testable without a real database (Testing Philosophy).
// Inner ring (use case) defines what it needs, in its own vocabulary:
public interface UserRepository {
Optional<User> findById(String id);
}
public class RegisterUserInteractor {
private final UserRepository users; // depends on the abstraction, not on SQL
RegisterUserInteractor(UserRepository users) { this.users = users; }
// ... business rule uses `users` without knowing it is Postgres, Mongo, or memory
}
// Outer ring (frameworks & drivers) implements it:
public class PostgresUserRepository implements UserRepository {
public Optional<User> findById(String id) { /* SQL here */ }
}