La arquitectura limpia (Clean Architecture, Robert C. Martin, 2012) y la arquitectura cebolla (Onion Architecture, Jeffrey Palermo, 2008) son dos formulaciones, muy emparentadas con la hexagonal, de una misma gran idea: el dominio debe ser independiente de todo lo demás, y las dependencias deben apuntar siempre hacia el interior. Ambas representan la aplicación como círculos concéntricos, donde el centro alberga las reglas de negocio más estables y el exterior, los detalles más volátiles (frameworks, bases de datos, interfaces). Entender estos estilos es clave para construir sistemas que envejezcan bien, porque permiten cambiar la tecnología sin reescribir el negocio. En esta lección estudiaremos la regla de dependencia, los círculos concéntricos, compararemos Clean, Onion y Hexagonal, y veremos una estructura de paquetes concreta.
Contenido
- La idea común: dependencias hacia el interior
- La regla de dependencia
- Los círculos concéntricos (Clean Architecture)
- La arquitectura cebolla (Onion)
- Comparación: Clean vs Onion vs Hexagonal
- Estructura de paquetes de ejemplo
- Inversión de dependencias en acción
- Errores comunes y consejos
- Ejercicios
- Conclusión
- La idea común: dependencias hacia el interior
Las tres arquitecturas (Hexagonal, Onion y Clean) comparten un mismo principio rector:
- En el centro está el dominio (entidades y reglas de negocio): lo más estable y valioso.
- En el exterior están los detalles (UI, BD, frameworks, dispositivos): lo más volátil.
- Nada del interior conoce el exterior. El dominio no sabe que existe una base de datos, un framework web o un servicio de correo.
Esto produce sistemas donde lo que más cambia (la tecnología) no arrastra a lo que menos debería cambiar (las reglas de negocio).
- La regla de dependencia
La regla de dependencia (The Dependency Rule) es el corazón de Clean Architecture y se enuncia así:
Las dependencias del código fuente solo pueden apuntar hacia adentro, hacia las políticas de más alto nivel.
graph LR
UI[Frameworks y Drivers] --> IA[Adaptadores de Interfaz]
IA --> CU[Casos de Uso]
CU --> ENT[Entidades]
style ENT fill:#cde
style UI fill:#fddConsecuencias prácticas:
- Un círculo interior no puede nombrar nada de un círculo exterior. Las entidades no conocen los casos de uso; los casos de uso no conocen los controladores.
- Lo que cruza la frontera hacia adentro lo hace mediante abstracciones (interfaces) declaradas en el interior. Es, de nuevo, la inversión de dependencias.
- Los datos que cruzan fronteras son estructuras simples (DTOs), nunca entidades de un framework externo.
- Los círculos concéntricos (Clean Architecture)
Clean Architecture propone (al menos) cuatro anillos, de dentro hacia fuera:
| Anillo | Contenido | Estabilidad | Conoce a... |
|---|---|---|---|
| 1. Entidades | Reglas de negocio de toda la empresa | Máxima | Nada |
| 2. Casos de uso | Reglas de negocio de la aplicación | Alta | Entidades |
| 3. Adaptadores de interfaz | Controladores, presentadores, gateways | Media | Casos de uso (vía interfaces) |
| 4. Frameworks y drivers | Web, BD, dispositivos | Mínima | Adaptadores |
graph TB
subgraph Anillo4[4 - Frameworks y Drivers]
subgraph Anillo3[3 - Adaptadores de Interfaz]
subgraph Anillo2[2 - Casos de Uso]
Anillo1[1 - Entidades]
end
end
end- Entidades: objetos con las reglas más generales y duraderas (p. ej., una
Polizay su invariante). Sobrevivirían aunque cambiara toda la aplicación. - Casos de uso: orquestan las entidades para cumplir una funcionalidad concreta de esta aplicación (p. ej., "contratar póliza").
- Adaptadores de interfaz: convierten datos entre el formato de los casos de uso y el de la tecnología exterior (controladores, presentadores, repositorios).
- Frameworks y drivers: Spring, JPA, el navegador, la BD. Detalles reemplazables.
- La arquitectura cebolla (Onion)
La arquitectura cebolla de Palermo es anterior y muy similar. Sus capas, de dentro hacia fuera:
| Capa | Contenido |
|---|---|
| Modelo de dominio | Entidades y objetos de valor |
| Servicios de dominio | Reglas e interfaces (incluidas las de repositorio) |
| Servicios de aplicación | Orquestación de casos de uso |
| Infraestructura / UI / Tests | Implementaciones concretas |
graph TB
subgraph Infra[Infraestructura / UI / Tests]
subgraph SApp[Servicios de Aplicación]
subgraph SDom[Servicios de Dominio + Interfaces]
Modelo[Modelo de Dominio]
end
end
endEl rasgo más característico de Onion: las interfaces de repositorio se definen en el dominio, y la infraestructura las implementa. De nuevo, inversión de dependencias. Onion popularizó la idea de que la infraestructura es un detalle del anillo más externo, igual que la UI.
- Comparación: Clean vs Onion vs Hexagonal
| Aspecto | Hexagonal | Onion | Clean |
|---|---|---|---|
| Autor / año | Cockburn, 2005 | Palermo, 2008 | Martin, 2012 |
| Metáfora | Hexágono con lados | Capas de cebolla | Círculos concéntricos |
| Centro | Dominio + aplicación | Modelo de dominio | Entidades |
| Frontera externa | Puertos y adaptadores | Capa de infraestructura | Frameworks y drivers |
| Regla esencial | Adaptadores dependen del núcleo | Dependencias hacia el centro | Dependencias hacia adentro |
| Énfasis distintivo | Puertos primarios/secundarios y testabilidad | Infra como detalle externo | Separación entidades vs casos de uso |
La conclusión más importante: son la misma idea con distinto vocabulario y distinto énfasis. Las tres colocan el dominio en el centro, prohíben que el centro dependa del exterior y resuelven el cruce de fronteras con inversión de dependencias. Hexagonal pone el foco en los puertos y adaptadores; Onion, en que la infraestructura es periférica; Clean, en distinguir las reglas de empresa (entidades) de las reglas de aplicación (casos de uso). Equivalencias útiles:
- Puerto secundario (Hexagonal) ≈ interfaz de repositorio en el dominio (Onion) ≈ gateway en adaptadores de interfaz (Clean).
- Adaptador primario (Hexagonal) ≈ controlador en adaptadores de interfaz (Clean).
- Estructura de paquetes de ejemplo
Una organización de paquetes habitual que materializa estas ideas en Java:
com.fiatc.seguros
├── domain # ANILLO 1-2: dominio puro, sin frameworks
│ ├── model
│ │ └── Poliza.java # Entidad con sus invariantes
│ ├── service
│ │ └── TarificadorRiesgo.java # Servicio de dominio
│ └── repository
│ └── RepositorioPolizas.java # INTERFAZ (la define el dominio)
│
├── application # ANILLO 2: casos de uso
│ └── usecase
│ └── ContratarPolizaUseCase.java
│
├── infrastructure # ANILLO 4: detalles reemplazables
│ ├── persistence
│ │ ├── PolizaEntity.java # Entidad JPA (NO es la del dominio)
│ │ └── RepositorioPolizasJpa.java # IMPLEMENTA la interfaz del dominio
│ └── config
│ └── BeanConfig.java # Cableado / inyección
│
└── interfaces # ANILLO 3: adaptadores de entrada
└── rest
└── PolizaController.java # Traduce HTTP -> caso de usoPuntos a destacar de la estructura:
domainno importa nada deinfrastructureni deinterfaces. Esa es la verificación clave: si lo hiciera, habrías roto la regla de dependencia.- La interfaz
RepositorioPolizasvive endomain.repository, pero su implementaciónRepositorioPolizasJpavive eninfrastructure. El flujo de control va del dominio a la infraestructura; la dependencia de código, al revés. PolizaEntity(JPA) yPoliza(dominio) son clases distintas. Se mapean entre sí. Así, una anotación de JPA nunca contamina el dominio.
- Inversión de dependencias en acción
Veamos cómo el caso de uso (interior) usa la infraestructura (exterior) sin depender de ella.
// ANILLO 2 (application): el caso de uso solo conoce la INTERFAZ del dominio
package com.fiatc.seguros.application.usecase;
import com.fiatc.seguros.domain.model.Poliza;
import com.fiatc.seguros.domain.repository.RepositorioPolizas; // interfaz interior
import com.fiatc.seguros.domain.service.TarificadorRiesgo;
public class ContratarPolizaUseCase {
private final RepositorioPolizas repositorio; // abstracción del dominio
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); // regla de negocio
Poliza poliza = new Poliza(cliente, riesgo, prima); // entidad valida invariante
return repositorio.guardar(poliza); // llama hacia "afuera" vía interfaz
}
}// ANILLO 4 (infrastructure): implementa la interfaz; depende HACIA ADENTRO
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)); // mapeo dominio -> entidad JPA
return poliza;
}
}El razonamiento clave: el import cruza la frontera solo en una dirección. infrastructure importa de domain (hacia adentro, permitido). domain nunca importa de infrastructure (hacia afuera, prohibido). Esa asimetría es exactamente la regla de dependencia.
// Verificación automática con ArchUnit: el dominio no debe depender de infraestructura
@Test
void el_dominio_no_depende_de_la_infraestructura() {
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure..")
.check(new ClassFileImporter().importPackages("com.fiatc.seguros"));
}Este test falla la compilación de la arquitectura si alguien introduce una dependencia prohibida. Es la mejor defensa para que la regla no se erosione con el tiempo.
- Errores Comunes y Consejos
- Dejar que el dominio importe el framework. El error número uno. El anillo interior debe ser Java puro.
- Reutilizar la entidad JPA como entidad de dominio. Tienta por ahorrar mapeo, pero acopla el dominio a la persistencia. Mantenlas separadas.
- Crear capas vacías por dogma. Si un caso de uso solo reenvía a un repositorio (sin reglas), revisa si aporta valor; no caigas en la "capa sumidero" disfrazada.
- Confundir flujo de control con dependencia de código. El control va del centro al exterior (el caso de uso llama a
guardar); la dependencia de código va del exterior al centro (la infra implementa la interfaz). - Aplicar la ceremonia completa a un CRUD trivial. Estos estilos brillan con dominios ricos en reglas; en un CRUD simple pueden ser sobreingeniería.
- Consejo: automatiza la verificación de fronteras con ArchUnit o el sistema de módulos; la disciplina manual no escala.
- Ejercicios
Ejercicio 1. Indica, para cada dependencia, si está permitida por la regla de dependencia:
(a) application importa domain. (b) domain importa infrastructure. (c) interfaces importa application. (d) domain importa org.springframework.
Ejercicio 2. Establece la equivalencia entre estos términos de las tres arquitecturas: "puerto secundario", "interfaz de repositorio en el dominio", "gateway".
Ejercicio 3. Un compañero coloca la interfaz RepositorioPolizas en el paquete infrastructure. Explica por qué viola la regla de dependencia y dónde debería ir.
Soluciones
Solución 1. (a) Permitida (hacia adentro). (b) Prohibida (el dominio apuntaría hacia afuera). (c) Permitida (hacia adentro). (d) Prohibida (el dominio no puede depender de un framework externo).
Solución 2. Los tres designan el mismo concepto: una abstracción que el núcleo declara para hablar con el exterior (típicamente persistencia o servicios externos), implementada en el anillo más externo. "Puerto secundario" es el término hexagonal; "interfaz de repositorio en el dominio", el de Onion; "gateway", el de Clean.
Solución 3. Si la interfaz vive en infrastructure, entonces application/domain (interior) tendrían que importar infrastructure (exterior) para usarla, lo que invierte la dirección permitida de las dependencias. La interfaz debe vivir en el dominio (p. ej. domain.repository), y solo su implementación en infrastructure.
- Conclusión
Clean y Onion son, junto con la hexagonal, expresiones de una misma verdad arquitectónica: pon el dominio en el centro y haz que todas las dependencias apunten hacia adentro. Hemos visto la regla de dependencia, los círculos concéntricos de Clean, las capas de Onion, su equivalencia con los puertos y adaptadores hexagonales, y una estructura de paquetes verificable con ArchUnit que mantiene el dominio libre de frameworks. Con esto cerramos el recorrido por los estilos que protegen el núcleo de negocio. A partir de aquí, el curso avanza hacia los estilos distribuidos y orientados a eventos —microservicios, mensajería, CQRS y event sourcing—, donde estas mismas reglas de dependencia se aplican ahora a través de fronteras de red.
Curso de Arquitectura de Aplicaciones
Módulo 1: Fundamentos de la Arquitectura de Aplicaciones
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
