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

  1. The common idea: dependencies pointing inward
  2. The dependency rule
  3. The concentric circles (Clean Architecture)
  4. Onion architecture (Onion)
  5. Comparison: Clean vs Onion vs Hexagonal
  6. Example package structure
  7. Dependency inversion in action
  8. Common mistakes and tips
  9. Exercises
  10. Conclusion

  1. 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).

  1. 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:#fdd

Practical 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.

  1. 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 Poliza and 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.

  1. 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
    end

Onion'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.

  1. 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).

  1. 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 case

Points to highlight about the structure:

  • domain imports nothing from infrastructure or interfaces. That is the key verification: if it did, you would have broken the dependency rule.
  • The RepositorioPolizas interface lives in domain.repository, but its RepositorioPolizasJpa implementation lives in infrastructure. The flow of control goes from the domain to the infrastructure; the code dependency, the other way around.
  • PolizaEntity (JPA) and Poliza (domain) are distinct classes. They are mapped to each other. This way, a JPA annotation never contaminates the domain.

  1. 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.

  1. 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.

  1. 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.

  1. 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

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved