Clean Architecture (Robert C. Martin, 2012) and Onion Architecture (Jeffrey Palermo, 2008) are two formulations, closely related to hexagonal, of one and the same big idea: the domain must be independent of everything else, and dependencies must always point inward. Both represent the application as concentric circles, where the center houses the most stable business rules and the outside houses the most volatile details (frameworks, databases, interfaces). Understanding these styles is key to building systems that age well, because they allow changing the technology without rewriting the business. In this lesson we will study the dependency rule, the concentric circles, compare Clean, Onion, and Hexagonal, and see a concrete package structure.
Contents
- The common idea: dependencies pointing inward
- The dependency rule
- The concentric circles (Clean Architecture)
- Onion architecture (Onion)
- Comparison: Clean vs Onion vs Hexagonal
- Example package structure
- Dependency inversion in action
- Common mistakes and tips
- Exercises
- Conclusion
- The common idea: dependencies pointing inward
The three architectures (Hexagonal, Onion, and Clean) share the same guiding principle:
- At the center is the domain (entities and business rules): the most stable and valuable.
- On the outside are the details (UI, DB, frameworks, devices): the most volatile.
- Nothing on the inside knows the outside. The domain does not know that there is a database, a web framework, or a mail service.
This produces systems where what changes the most (the technology) does not drag along what should change the least (the business rules).
- The dependency rule
The dependency rule (The Dependency Rule) is the heart of Clean Architecture and is stated as follows:
Source code dependencies can only point inward, toward the higher-level policies.
graph LR
UI[Frameworks and Drivers] --> IA[Interface Adapters]
IA --> CU[Use Cases]
CU --> ENT[Entities]
style ENT fill:#cde
style UI fill:#fddPractical consequences:
- An inner circle cannot name anything from an outer circle. Entities do not know the use cases; use cases do not know the controllers.
- What crosses the boundary inward does so through abstractions (interfaces) declared on the inside. It is, again, dependency inversion.
- The data that crosses boundaries are simple structures (DTOs), never entities from an external framework.
- The concentric circles (Clean Architecture)
Clean Architecture proposes (at least) four rings, from the inside out:
| Ring | Content | Stability | Knows... |
|---|---|---|---|
| 1. Entities | Enterprise-wide business rules | Maximum | Nothing |
| 2. Use cases | Application business rules | High | Entities |
| 3. Interface adapters | Controllers, presenters, gateways | Medium | Use cases (via interfaces) |
| 4. Frameworks and drivers | Web, DB, devices | Minimal | Adapters |
graph TB
subgraph Anillo4[4 - Frameworks and Drivers]
subgraph Anillo3[3 - Interface Adapters]
subgraph Anillo2[2 - Use Cases]
Anillo1[1 - Entities]
end
end
end- Entities: objects with the most general and long-lasting rules (e.g., a
Polizaand its invariant). They would survive even if the entire application changed. - Use cases: orchestrate the entities to fulfill a specific feature of this application (e.g., "underwrite policy").
- Interface adapters: convert data between the format of the use cases and that of the external technology (controllers, presenters, repositories).
- Frameworks and drivers: Spring, JPA, the browser, the DB. Replaceable details.
- Onion architecture (Onion)
Palermo's Onion architecture is earlier and very similar. Its layers, from the inside out:
| Layer | Content |
|---|---|
| Domain model | Entities and value objects |
| Domain services | Rules and interfaces (including repository ones) |
| Application services | Use case orchestration |
| Infrastructure / UI / Tests | Concrete implementations |
graph TB
subgraph Infra[Infrastructure / UI / Tests]
subgraph SApp[Application Services]
subgraph SDom[Domain Services + Interfaces]
Modelo[Domain Model]
end
end
endOnion's most characteristic trait: the repository interfaces are defined in the domain, and the infrastructure implements them. Again, dependency inversion. Onion popularized the idea that the infrastructure is a detail of the outermost ring, just like the UI.
- Comparison: Clean vs Onion vs Hexagonal
| Aspect | Hexagonal | Onion | Clean |
|---|---|---|---|
| Author / year | Cockburn, 2005 | Palermo, 2008 | Martin, 2012 |
| Metaphor | Hexagon with sides | Onion layers | Concentric circles |
| Center | Domain + application | Domain model | Entities |
| External boundary | Ports and adapters | Infrastructure layer | Frameworks and drivers |
| Essential rule | Adapters depend on the core | Dependencies toward the center | Dependencies inward |
| Distinctive emphasis | Primary/secondary ports and testability | Infra as an external detail | Separating entities vs use cases |
The most important conclusion: they are the same idea with different vocabulary and different emphasis. All three place the domain at the center, forbid the center from depending on the outside, and resolve boundary crossing with dependency inversion. Hexagonal focuses on ports and adapters; Onion, on the infrastructure being peripheral; Clean, on distinguishing enterprise rules (entities) from application rules (use cases). Useful equivalences:
- Secondary port (Hexagonal) ≈ repository interface in the domain (Onion) ≈ gateway in interface adapters (Clean).
- Primary adapter (Hexagonal) ≈ controller in interface adapters (Clean).
- Example package structure
A common package organization that materializes these ideas in Java:
com.fiatc.seguros
├── domain # RING 1-2: pure domain, no frameworks
│ ├── model
│ │ └── Poliza.java # Entity with its invariants
│ ├── service
│ │ └── TarificadorRiesgo.java # Domain service
│ └── repository
│ └── RepositorioPolizas.java # INTERFACE (defined by the domain)
│
├── application # RING 2: use cases
│ └── usecase
│ └── ContratarPolizaUseCase.java
│
├── infrastructure # RING 4: replaceable details
│ ├── persistence
│ │ ├── PolizaEntity.java # JPA entity (NOT the domain one)
│ │ └── RepositorioPolizasJpa.java # IMPLEMENTS the domain interface
│ └── config
│ └── BeanConfig.java # Wiring / injection
│
└── interfaces # RING 3: inbound adapters
└── rest
└── PolizaController.java # Translates HTTP -> use casePoints to highlight about the structure:
domainimports nothing frominfrastructureorinterfaces. That is the key verification: if it did, you would have broken the dependency rule.- The
RepositorioPolizasinterface lives indomain.repository, but itsRepositorioPolizasJpaimplementation lives ininfrastructure. The flow of control goes from the domain to the infrastructure; the code dependency, the other way around. PolizaEntity(JPA) andPoliza(domain) are distinct classes. They are mapped to each other. This way, a JPA annotation never contaminates the domain.
- Dependency inversion in action
Let's see how the use case (inside) uses the infrastructure (outside) without depending on it.
// RING 2 (application): the use case knows only the domain INTERFACE
package com.fiatc.seguros.application.usecase;
import com.fiatc.seguros.domain.model.Poliza;
import com.fiatc.seguros.domain.repository.RepositorioPolizas; // inner interface
import com.fiatc.seguros.domain.service.TarificadorRiesgo;
public class ContratarPolizaUseCase {
private final RepositorioPolizas repositorio; // domain abstraction
private final TarificadorRiesgo tarificador;
public ContratarPolizaUseCase(RepositorioPolizas r, TarificadorRiesgo t) {
this.repositorio = r; this.tarificador = t;
}
public Poliza ejecutar(String cliente, String riesgo) {
double prima = tarificador.calcularPrima(riesgo); // business rule
Poliza poliza = new Poliza(cliente, riesgo, prima); // entity validates invariant
return repositorio.guardar(poliza); // calls "outward" via the interface
}
}// RING 4 (infrastructure): implements the interface; depends INWARD
package com.fiatc.seguros.infrastructure.persistence;
import com.fiatc.seguros.domain.model.Poliza;
import com.fiatc.seguros.domain.repository.RepositorioPolizas;
import org.springframework.stereotype.Repository;
@Repository
class RepositorioPolizasJpa implements RepositorioPolizas {
private final JpaPolizaDao dao;
RepositorioPolizasJpa(JpaPolizaDao dao) { this.dao = dao; }
@Override public Poliza guardar(Poliza poliza) {
dao.save(PolizaEntity.desde(poliza)); // domain -> JPA entity mapping
return poliza;
}
}The key reasoning: the import crosses the boundary in only one direction. infrastructure imports from domain (inward, allowed). domain never imports from infrastructure (outward, forbidden). That asymmetry is exactly the dependency rule.
// Automatic verification with ArchUnit: the domain must not depend on infrastructure
@Test
void el_dominio_no_depende_de_la_infraestructura() {
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure..")
.check(new ClassFileImporter().importPackages("com.fiatc.seguros"));
}This test fails the architecture build if someone introduces a forbidden dependency. It is the best defense to keep the rule from eroding over time.
- Common Mistakes and Tips
- Letting the domain import the framework. The number-one mistake. The inner ring must be pure Java.
- Reusing the JPA entity as the domain entity. It is tempting to save on mapping, but it couples the domain to persistence. Keep them separate.
- Creating empty layers out of dogma. If a use case only forwards to a repository (with no rules), check whether it adds value; do not fall into the disguised "sinkhole layer."
- Confusing flow of control with code dependency. Control goes from the center to the outside (the use case calls
guardar); the code dependency goes from the outside to the center (the infra implements the interface). - Applying the full ceremony to a trivial CRUD. These styles shine with rule-rich domains; in a simple CRUD they can be over-engineering.
- Tip: automate boundary verification with ArchUnit or the module system; manual discipline does not scale.
- Exercises
Exercise 1. Indicate, for each dependency, whether it is allowed by the dependency rule:
(a) application imports domain. (b) domain imports infrastructure. (c) interfaces imports application. (d) domain imports org.springframework.
Exercise 2. Establish the equivalence between these terms from the three architectures: "secondary port", "repository interface in the domain", "gateway".
Exercise 3. A colleague places the RepositorioPolizas interface in the infrastructure package. Explain why this violates the dependency rule and where it should go.
Solutions
Solution 1. (a) Allowed (inward). (b) Forbidden (the domain would point outward). (c) Allowed (inward). (d) Forbidden (the domain cannot depend on an external framework).
Solution 2. All three designate the same concept: an abstraction that the core declares to talk to the outside (typically persistence or external services), implemented in the outermost ring. "Secondary port" is the hexagonal term; "repository interface in the domain", the Onion one; "gateway", the Clean one.
Solution 3. If the interface lives in infrastructure, then application/domain (inside) would have to import infrastructure (outside) to use it, which inverts the allowed direction of the dependencies. The interface must live in the domain (e.g., domain.repository), and only its implementation in infrastructure.
- Conclusion
Clean and Onion are, together with hexagonal, expressions of one and the same architectural truth: put the domain at the center and make all dependencies point inward. We have seen the dependency rule, Clean's concentric circles, Onion's layers, their equivalence with hexagonal ports and adapters, and a package structure verifiable with ArchUnit that keeps the domain free of frameworks. With this we close the tour of the styles that protect the business core. From here on, the course advances toward the distributed and event-oriented styles—microservices, messaging, CQRS, and event sourcing—where these same dependency rules now apply across network boundaries.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
