Lección 2 de 914 min
Organizar el código: capas, hexagonal y Clean
Arquitectura en capas, hexagonal, Clean, Onion y Domain-Driven Design: proteger la lógica de negocio.
Llevas un año con tu aplicación y quieres cambiar de proveedor de pagos. Descubres que la regla «cuándo se puede reembolsar un pedido» está repartida entre un controlador HTTP, una consulta SQL y un componente de la interfaz. Cambiar una pieza obliga a tocarlas todas.
Las arquitecturas de esta lección organizan el código dentro de una aplicación, sea un monolito o un microservicio, para proteger lo más valioso: la lógica de negocio. Todas responden a la misma pregunta: ¿qué partes pueden depender de cuáles?
Capas, hexágonos y anillos
Las cuatro dividen el código en zonas con reglas sobre quién puede importar a quién. La diferencia está en hacia dónde apuntan esas dependencias.
Arquitectura en capas
Presentación, negocio y datos apilados: cada capa solo usa la de debajo.
- Capas típicas:
- Presentación, negocio, datos
- Dependencias:
- Hacia abajo
La arquitectura en capas (layered, o n-tier) divide el código en niveles horizontales. La versión clásica tiene tres: presentación (controladores, vistas), negocio (servicios con las reglas) y acceso a datos (repositorios, consultas). Cada capa solo llama a la inmediatamente inferior.
Es la estructura que proponen por defecto muchos frameworks, como Spring, NestJS o ASP.NET, y la más fácil de entender: cuando llega una petición, sabes exactamente por dónde pasa.
Úsalo cuando
En aplicaciones de gestión y CRUD con poca lógica de negocio, o como primer paso para poner orden en un proyecto sin estructura.
Ten en cuenta
La capa de negocio depende de la de datos, así que las reglas acaban atadas a la base de datos y al ORM, y no puedes probarlas sin ellos. También es fácil acabar con servicios que solo pasan datos de arriba abajo sin aportar nada.
// Presentación: recibe la petición y responde
app.post("/pedidos/:id/reembolso", async (req, res) => {
res.json(await servicioPedidos.reembolsar(req.params.id));
});
// Negocio: las reglas
class ServicioPedidos {
async reembolsar(id: string) {
const pedido = await repositorioPedidos.buscar(id); // depende de la capa de datos
if (pedido.diasDesdeCompra > 30) throw new Error("Fuera de plazo");
return repositorioPedidos.marcarReembolsado(id);
}
}Hexagonal (puertos y adaptadores)
El núcleo define puertos; el exterior se conecta con adaptadores intercambiables.
- Autor:
- Alistair Cockburn
- También llamada:
- Puertos y adaptadores
Alistair Cockburn propuso la arquitectura hexagonal con una idea: la aplicación no debería saber si la usa una persona desde la web, un test o un script, ni si sus datos van a PostgreSQL o a un archivo. El núcleo define puertos, interfaces que expresan lo que necesita («guardar un pedido») o lo que ofrece («crear un pedido»).
Los adaptadores implementan esos puertos con una tecnología concreta. Los de entrada (driving) llaman al núcleo: un controlador HTTP, una CLI, un consumidor de mensajes. Los de salida (driven) son llamados por él: un repositorio SQL, un cliente de correo. El hexágono es solo un dibujo: podrían ser cinco lados o veinte.
Úsalo cuando
Cuando la lógica de negocio es valiosa y quieres probarla sin base de datos ni red, o cuando prevés cambiar de tecnología de infraestructura.
Ten en cuenta
Para un CRUD sencillo, tantas interfaces son pura ceremonia. No crees un puerto para cada cosa: créalo donde de verdad quieras intercambiar o simular la pieza.
// Puerto de salida: lo define el núcleo, con sus palabras
export interface RepositorioPedidos {
buscar(id: string): Promise<Pedido | null>;
guardar(pedido: Pedido): Promise<void>;
}
// Adaptador de salida: implementa el puerto con PostgreSQL
export class PedidosPostgres implements RepositorioPedidos {
constructor(private db: Pool) {}
async buscar(id: string) { /* SELECT … */ }
async guardar(pedido: Pedido) { /* INSERT … */ }
}
// Adaptador para los tests: el mismo puerto, en memoria
export class PedidosEnMemoria implements RepositorioPedidos {
private datos = new Map<string, Pedido>();
async buscar(id: string) { return this.datos.get(id) ?? null; }
async guardar(pedido: Pedido) { this.datos.set(pedido.id, pedido); }
}Clean Architecture
Círculos concéntricos y una regla: las dependencias siempre apuntan hacia dentro.
- Autor:
- Robert C. Martin
- Desde:
- 2012
Robert C. Martin la presentó en 2012 como una síntesis de ideas anteriores, la hexagonal entre ellas. Se dibuja como círculos: en el centro, las entidades (las reglas de negocio); alrededor, los casos de uso (lo que hace la aplicación, como «RealizarPedido»); después, los adaptadores de interfaz (controladores, presentadores, gateways); y fuera, los frameworks y drivers: la web, la base de datos, la UI.
Su pieza clave es la regla de dependencia: el código de un círculo solo puede conocer el de círculos más internos. Una entidad nunca importa nada de Express, de Angular ni del ORM. Cuando el núcleo necesita algo de fuera, declara una interfaz y el exterior la implementa: es la inversión de dependencias.
Úsalo cuando
En aplicaciones de vida larga con reglas de negocio complejas, donde quieres que los frameworks sean un detalle reemplazable y que cada caso de uso se vea de un vistazo.
Ten en cuenta
Aplicada al pie de la letra genera muchas capas, conversiones entre objetos casi idénticos y carpetas vacías. Lo importante es la regla de dependencia, no el número de círculos.
// Caso de uso: solo conoce entidades e interfaces que él mismo declara
export class RealizarPedido {
constructor(
private pedidos: RepositorioPedidos, // una interfaz, no PostgreSQL
private pagos: PasarelaPago, // una interfaz, no un proveedor concreto
) {}
async ejecutar(entrada: { clienteId: string; lineas: Linea[] }) {
const pedido = Pedido.crear(entrada.clienteId, entrada.lineas);
await this.pagos.cobrar(pedido.total);
await this.pedidos.guardar(pedido);
return pedido.id;
}
}Onion Architecture
Capas en forma de cebolla con el modelo de dominio en el centro.
- Autor:
- Jeffrey Palermo
- Desde:
- 2008
Jeffrey Palermo la presentó en 2008. Sus anillos, de dentro afuera, son el modelo de dominio, los servicios de dominio, los servicios de aplicación y, en la piel, la infraestructura, la interfaz y los tests. Como en Clean, las dependencias apuntan siempre al centro.
Nació como respuesta a la arquitectura en capas tradicional, en la que todo acababa dependiendo de la base de datos. En la cebolla, la base de datos es un detalle externo: el dominio declara las interfaces de sus repositorios y la infraestructura las implementa.
Úsalo cuando
En proyectos con un dominio rico, sobre todo cuando se combina con Domain-Driven Design. Es muy habitual en el ecosistema .NET.
Ten en cuenta
Comparte los riesgos de Clean: demasiadas capas para poca lógica. Si dudas entre Onion y Clean, no le des muchas vueltas: la idea de fondo es la misma.
// dominio/pedido.ts: el centro, sin imports de fuera
export class Pedido { /* reglas de negocio */ }
// aplicacion/cancelar-pedido.ts: solo mira hacia dentro
import { Pedido } from "../dominio/pedido";
import type { RepositorioPedidos } from "../dominio/repositorio-pedidos";
// infraestructura/pedidos-sql.ts: la piel conoce a todos
import type { RepositorioPedidos } from "../dominio/repositorio-pedidos";
export class PedidosSql implements RepositorioPedidos { /* … */ }La misma idea con tres dibujos
Hexagonal, Clean y Onion dibujan cosas distintas (un hexágono, círculos, una cebolla), pero comparten el núcleo: la lógica de negocio va en el centro, no depende de ningún framework ni base de datos, y las dependencias apuntan hacia dentro gracias a interfaces que el centro declara y el exterior implementa. Si entiendes una, entiendes las tres.
Domain-Driven Design
Las arquitecturas anteriores dicen dónde poner la lógica de negocio. Domain-Driven Design (DDD), propuesto por Eric Evans en su libro de 2003, dice cómo descubrirla y modelarla. Tiene una parte estratégica (el lenguaje y las fronteras) y otra táctica (los bloques con los que escribes el código).
DDD y el lenguaje ubicuo
Modelar el software con las mismas palabras que usan los expertos del negocio.
- Libro:
- Eric Evans, 2003
Domain-Driven Design propone que el diseño parta del dominio, el problema de negocio que resuelve el software, trabajando codo con codo con quienes lo conocen: el equipo de logística, los agentes de seguros, el personal de recepción de un hotel.
Su primera herramienta es el lenguaje ubicuo: un vocabulario común que usan por igual negocio y desarrollo, en las reuniones y en el código. Si en el almacén hablan de «ubicar un palé», tu código tiene palet.ubicar(hueco), no updateLocation(itemId, 7). Cuando una palabra resulta ambigua, se discute y se acuerda.
Úsalo cuando
En dominios complejos y cambiantes (seguros, logística, banca) donde entender bien el negocio es la parte difícil del proyecto.
Ten en cuenta
DDD no es una arquitectura ni una carpeta llamada domain. En una aplicación sencilla de formularios, sus patrones tácticos añaden más trabajo que valor.
// Sin lenguaje ubicuo: nadie de recepción entendería esto
reserva.estado = 3;
reserva.flag2 = true;
// Con lenguaje ubicuo: así lo dice el equipo del hotel
reserva.confirmar();
reserva.registrarNoShow(); // «no show»: el cliente no se presentóContextos delimitados
Fronteras dentro de las cuales cada palabra del negocio tiene un único significado.
- En inglés:
- Bounded context
En una empresa grande, una misma palabra significa cosas distintas según a quién preguntes. Para Ventas, un «cliente» es alguien con un historial de compras; para Facturación, un número fiscal y una dirección de facturación; para Soporte, una lista de incidencias. Un único modelo Cliente que sirva a todos acaba siendo una clase gigante que nadie entiende.
Un contexto delimitado (bounded context) es la frontera dentro de la cual un modelo y su lenguaje son coherentes. Cada contexto tiene su propio «cliente», y entre contextos los datos se traducen de forma explícita. Un mapa de contextos documenta cómo se relacionan.
Úsalo cuando
Siempre que varias áreas del negocio compartan conceptos con matices distintos. Los contextos delimitados son, además, el mejor punto de partida para trazar las fronteras de módulos o microservicios.
Ten en cuenta
Un contexto no es una tabla ni una entidad: es un área del negocio. Si lo partes en contextos demasiado pequeños, pasarás el día traduciendo datos de uno a otro.
// contextos/ventas/cliente.ts
export type Cliente = {
id: string;
nombre: string;
historialCompras: PedidoResumen[];
};
// contextos/facturacion/cliente.ts
export type Cliente = {
id: string; // el mismo id permite relacionarlos
razonSocial: string;
numeroFiscal: string;
direccionFiscal: Direccion;
};Entidades y objetos de valor
Lo que tiene identidad propia frente a lo que solo vale por sus atributos.
Una entidad tiene una identidad que se mantiene en el tiempo aunque cambien sus datos: un pedido sigue siendo el mismo pedido aunque cambies la dirección de entrega. Dos entidades con los mismos datos pero distinto id son distintas.
Un objeto de valor (value object) no tiene identidad: lo define por completo su contenido. Diez euros son diez euros, y dos direcciones con la misma calle y ciudad son la misma dirección. Se hacen inmutables (para cambiarlos, creas uno nuevo) y se comparan por valor. Son el sitio perfecto para validar: un Email que no puede existir si no es válido.
Úsalo cuando
Usa objetos de valor para dinero, rangos de fechas, direcciones, coordenadas o emails, en lugar de números y textos sueltos. Reserva las entidades para lo que el negocio sigue a lo largo del tiempo.
Ten en cuenta
El error más común es modelarlo todo como entidad, con id y setters. Y nunca representes dinero como un simple number sin moneda: en coma flotante, 0.1 + 0.2 no da exactamente 0.3.
// Objeto de valor: inmutable y comparado por su contenido
export class Dinero {
constructor(readonly centimos: number, readonly moneda: "EUR" | "USD") {}
sumar(otro: Dinero) {
if (otro.moneda !== this.moneda) throw new Error("Monedas distintas");
return new Dinero(this.centimos + otro.centimos, this.moneda); // uno nuevo
}
esIgualA(otro: Dinero) {
return this.centimos === otro.centimos && this.moneda === otro.moneda;
}
}Agregados
Un grupo de objetos que cambia como una unidad, con una raíz que protege sus reglas.
- Regla de oro:
- Una transacción, un agregado
Un agregado agrupa entidades y objetos de valor que deben mantenerse coherentes entre sí. Un pedido con sus líneas es el ejemplo típico: la regla «el total no puede superar el límite de crédito» afecta a todas las líneas a la vez. Una de las entidades es la raíz del agregado y es la única puerta de entrada: nadie modifica una línea sin pasar por el pedido.
Tres reglas prácticas: se guarda y se carga entero, en una sola transacción; desde fuera solo se referencia por el id de su raíz; y se diseña lo más pequeño posible. Si dos agregados deben coordinarse, se hace con eventos y consistencia eventual.
Úsalo cuando
Cuando hay reglas de negocio (invariantes, condiciones que siempre deben cumplirse) que abarcan varios objetos y no quieres que ningún camino del código pueda saltárselas.
Ten en cuenta
Los agregados enormes, como un Cliente con todos sus pedidos dentro, provocan cargas lentas y conflictos cuando dos personas los modifican a la vez. Si una regla no exige coherencia inmediata, sepárala en otro agregado.
export class Pedido {
private lineas: Linea[] = [];
constructor(readonly id: string, private limiteCredito: Dinero) {}
// La raíz es la única forma de añadir líneas: así protege la regla
agregarLinea(producto: ProductoId, cantidad: number, precio: Dinero) {
const nuevoTotal = this.total().sumar(precio.multiplicar(cantidad));
if (nuevoTotal.centimos > this.limiteCredito.centimos) {
throw new Error("El pedido supera el límite de crédito");
}
this.lineas.push(new Linea(producto, cantidad, precio));
}
total(): Dinero { /* suma de las líneas */ }
}Nota
DDD y las arquitecturas del grupo anterior encajan bien: los contextos delimitados marcan las fronteras entre módulos o servicios y, dentro de cada uno, una arquitectura hexagonal o Clean protege el modelo de dominio de la infraestructura.
Interactivo
Capas, hexagonal, Clean y Onion
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Capas | Hexagonal | Clean |
|---|---|---|---|
| Imagen | Pisos apilados | Un hexágono con puertos | Círculos concéntricos |
| Hacia dónde apuntan las dependencias | Hacia abajo: el negocio depende de los datos | Hacia el núcleo | Hacia dentro (regla de dependencia) |
| Lugar de la base de datos | En la base de la pila | Fuera: un adaptador de salida | En el círculo exterior |
| Vocabulario propio | Presentación, negocio, datos | Puertos, adaptadores de entrada y de salida | Entidades, casos de uso, adaptadores |
| Cuándo compensa | CRUD y lógica sencilla | Lógica valiosa que quieres probar sin infraestructura | Aplicaciones grandes y de vida larga |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Un panel interno para que tres personas den de alta proveedores y consulten listados. Casi no hay reglas de negocio.
Para recordar
- En capas, el negocio depende de los datos; en hexagonal, Clean y Onion, la infraestructura depende del negocio.
- Hexagonal, Clean y Onion son la misma idea con distinto dibujo: el núcleo declara interfaces y el exterior las implementa.
- DDD empieza por el lenguaje: el código habla como el negocio, y cada contexto delimitado tiene su propio modelo.
- Usa objetos de valor inmutables para dinero, fechas o direcciones, y agregados pequeños para proteger las reglas que abarcan varios objetos.
- Más capas no es mejor arquitectura: aplica estas ideas donde haya lógica que merezca protección.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué dice la regla de dependencia de Clean Architecture?
2.En la arquitectura hexagonal, ¿qué es un controlador HTTP?
3.¿Cuál de estos conceptos se modela mejor como objeto de valor?
4.¿Por qué se recomienda que los agregados sean pequeños?
5.¿Qué tienen en común las arquitecturas hexagonal, Clean y Onion?