La mayoría de los ingenieros han visto la imagen de la clean architecture: cuatro anillos de colores con flechas que apuntan al centro. Muchos menos saben decir qué entra en cada anillo en una funcionalidad real, hacia dónde deben apuntar los imports o dónde vive el SQL.
La brecha se nota en la revisión de código: una carpeta domain que importa el ORM, un use case que devuelve un código de estado HTTP, cinco mappers entre un formulario y una tabla.
Este artículo convierte los círculos de Robert C. Martin en un diagrama de componentes para un use case, hacer un pedido: las capas, la regla de dependencia, código y carpetas, cruces de frontera, clean frente a hexagonal y onion, errores, cuándo es excesivo y cómo dibujarlo.
Las cuatro capas
Martin describió la clean architecture en una entrada de blog de 2012, The Clean Architecture, y la amplió en su libro de 2017 Clean Architecture. La presentó como una integración de ideas anteriores, entre ellas la arquitectura hexagonal y la onion. Del centro hacia fuera:
| Capa | Qué contiene | En el ejemplo | Cambia cuando |
|---|---|---|---|
| Entities | Reglas de negocio de la empresa, reutilizables entre aplicaciones | Order y su regla de que un pedido necesita al menos una línea |
Cambia una regla de negocio |
| Use cases | Reglas de negocio de la aplicación, un flujo cada uno | PlaceOrder, sus dos interfaces, los modelos de solicitud y de respuesta |
Cambia el flujo |
| Interface adapters | Controladores, presenters, gateways y todo el SQL | OrderController, OrderPresenter, PostgresOrderRepository |
Cambia la API o el esquema |
| Frameworks and drivers | Herramientas más código de unión: framework web, base de datos, drivers | El framework HTTP, PostgreSQL, pg, main.ts |
Sustituyes una herramienta |
Los círculos son esquemáticos; Martin dice que quizá necesites más de cuatro. La regla que los conecta nunca cambia.
La regla de dependencia
En palabras de Martin, «las dependencias del código fuente solo pueden apuntar hacia dentro». El código de un círculo interno nunca menciona una función, clase o variable declarada en uno externo, ni usa sus formatos de datos. Para este ejemplo:
entitiesno importa nada de las otras capas.use-casessolo importaentities: ni framework web, ni ORM, ni driver.- Los
adaptersimportan hacia dentro, más la biblioteca que adaptan. - Solo
main.tslo ve todo.
La regla cubre el código fuente, no las llamadas en tiempo de ejecución: en ejecución, PlaceOrder llama hacia fuera, al presenter y a la base de datos. Martin lo resuelve con el principio de inversión de dependencias: el use case llama a una interfaz que le pertenece, y una clase externa la implementa. La llamada sale; la dependencia apunta hacia dentro.
Una dependencia sí apunta hacia fuera: el adapter importa el driver de la base de datos. Es a propósito. Martin pone todo el SQL en la capa de adapters, y ningún código más interno debería «saber nada en absoluto sobre la base de datos».
Haz cumplir la regla en CI con la comprobación onionArchitecture() de ArchUnit en la JVM, dependency-cruiser para TypeScript o el contrato layers de import-linter para Python.
Ejemplo completo: hacer un pedido
Una solicitud POST /orders sigue este camino:
- El framework web la enruta a
OrderController. - El controlador convierte el cuerpo en un
PlaceOrderRequest, un objeto simple que pertenece a la capa de use cases, crea unOrderPresentery llama aPlaceOrder.execute(). PlaceOrderdeja queOrder.place()aplique las reglas de negocio, guarda a través deOrderRepositoryy pasa unPlaceOrderResponseaPlaceOrderOutput.OrderPresenterimplementa esa interfaz y construye un view model: estado 201, una cabeceraLocationy un cuerpo JSON.- El controlador devuelve el view model, y el framework lo escribe.
La capa de use cases, con las dos interfaces que le pertenecen:
// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';
export interface OrderRepository { // implemented by an adapter
nextId(): Promise<string>;
save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput { // implemented by the presenter
present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
customerId: string;
lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };
export class PlaceOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
await this.orders.save(order);
output.present({ orderId: order.id, itemCount: order.itemCount() });
}
}
Solo importa la entity. Order.place() lanza un error cuando un pedido no tiene líneas o tiene una cantidad inferior a uno, y el archivo de la entity no importa nada. El adapter importa hacia dentro, más el driver:
// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg'; // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async nextId(): Promise<string> { return randomUUID(); }
async save(order: Order): Promise<void> {
await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
[order.id, order.customerId, JSON.stringify(order.lines)]);
}
}
Solo el composition root conoce cada clase concreta:
// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));
app.post('/orders', async (req, res) => {
const view = await controller.post(req.body); // the presenter's view model
res.status(view.status).set(view.headers).json(view.body);
});
Una prueba unitaria de PlaceOrder solo necesita un repositorio en memoria y un presenter que registre su entrada: sin servidor, sin base de datos.
Estructura de carpetas
src/
orders/
entities/
order.ts Order and its rules
use-cases/
place-order.ts PlaceOrder, both interfaces, request and response models
adapters/
http/
order-controller.ts
order-presenter.ts
postgres/
postgres-order-repository.ts
main.ts composition root: builds and wires everything
Agrupa por funcionalidad y luego por capa. El argumento de la screaming architecture de Martin: las carpetas de primer nivel deben decir qué hace el sistema (orders, payments), no qué framework usa. El capítulo de Simon Brown en el mismo libro separa la organización del encapsulamiento: las carpetas organizan el código, mientras que las reglas de acceso o una comprobación de build lo hacen cumplir.
Cruzar fronteras
Cada cruce de frontera lleva un tipo que pertenece a una capa:
| Cruce | Qué viaja | Tipo propiedad de | Mecanismo |
|---|---|---|---|
| Framework → controlador | Solicitud HTTP | Framework | Código de unión en main.ts |
| Controlador → use case | PlaceOrderRequest |
Use cases | Llamada directa |
| Use case → presenter | PlaceOrderResponse |
Use cases | Interfaz PlaceOrderOutput |
| Use case → base de datos | Order |
Entities | Interfaz OrderRepository |
| Presenter → framework | View model | Adapters | Lo devuelve el controlador |
Tres hábitos mantienen limpios estos cruces:
- Modelos de solicitud y respuesta simples. Martin es tajante: «No queremos hacer trampa y pasar entities o filas de la base de datos». El presenter recibe
PlaceOrderResponse, nuncaOrder, así que renombrar un campo de la entity no toca la API. Los repositorios son una excepción común: muchas bases de código les permiten aceptar entities, lo que sigue apuntando hacia dentro. Los equipos más estrictos lo asignan a un registro de persistencia dentro del adapter. - Una interfaz en cada llamada hacia fuera, propiedad de la capa interna y nombrada según lo que necesita el use case, no según la tecnología.
- Un solo composition root. Mark Seemann define el composition root como «un lugar (preferiblemente) único en una aplicación donde se componen los módulos», cerca del punto de entrada. Martin llama a
main«el detalle definitivo». Todo lo demás recibe sus dependencias por constructor.
La figura de Martin también tiene una interfaz Use Case Input Port delante del interactor. Con un controlador y una implementación, llamar directamente a la clase es una simplificación común.
Clean frente a hexagonal y onion
| Hexagonal (ports and adapters) | Onion | Clean | |
|---|---|---|---|
| Texto fundacional | Alistair Cockburn, artículo de 2005 | Jeffrey Palermo, serie de blog de 2008 | Robert C. Martin, entrada de blog de 2012, libro de 2017 |
| Imagen | Aplicación en un hexágono, adapters fuera | Anillos alrededor del modelo de dominio | Cuatro círculos concéntricos |
| Estructura interna | No prescrita | Modelo de dominio, servicios de dominio, servicios de aplicación | Entities, use cases |
| Implementación del repositorio | Adapter secundario (dirigido) | Infraestructura, anillo exterior | Interface adapters |
| Regla central | El código interno no debe filtrarse hacia fuera; se comunica por ports | Todo acoplamiento apunta al centro | Las dependencias del código fuente apuntan hacia dentro |
Las tres ponen las reglas de negocio en el centro y hacen que los bordes dependan de ellas. Se diferencian en cuánta estructura prescriben por dentro: el artículo original de Cockburn solo separa el interior del exterior, la onion architecture de Palermo nombra anillos alrededor del modelo de dominio, y la clean architecture añade presenters y modelos de solicitud y respuesta. El diagrama de arquitectura hexagonal recorre un ejemplo de pedido similar en términos de ports and adapters.
Errores comunes
- Tipos de framework en las entities. Los decoradores del ORM o los atributos de serialización en
Orderatan la capa más interna a la más externa. Asigna en el adapter. - Un mapper para todo. Seis copias de los mismos campos para un formulario que guarda una fila. Asigna solo donde las formas difieren.
- Capas sin comportamiento. Entities de getters y setters, el modelo de dominio anémico que describió Martin Fowler en 2003, y use cases que solo llaman a
save(). - Interfaces por todas partes. Pertenecen a las fronteras, no a cada clase.
- HTTP en el use case. Los códigos de estado pertenecen al presenter.
Cuándo es excesivo
Palermo escribió que la onion architecture «no es apropiada para sitios web pequeños», y el capítulo de Martin sobre fronteras parciales empieza con «Las fronteras arquitectónicas completas son caras».
| Situación | Mejor opción |
|---|---|
| Servicio CRUD con pocas reglas | Una aplicación en capas delgada; mantén las pocas reglas fuera de los controladores |
| Prototipo o herramienta de corta vida | Sin capas |
| Una dependencia volátil, como un proveedor de pagos | Una interfaz y un adapter para ella |
| Producto de larga vida con reglas reales y varios puntos de entrada | Las cuatro capas |
Ese capítulo también describe fronteras parciales más baratas; promueve una cuando una segunda implementación o una prueba la necesite.
El diagrama de clean architecture
Contenido del diagrama
- Frameworks and drivers
- Interface adapters
- Use cases
- Entities
- Web framework
- PostgreSQL
- OrderPresenter
- OrderController
- PostgresOrderRepository
- PlaceOrderOutput
- PlaceOrder
- OrderRepository
- Order
- POST /orders
- execute(request)
- Order.place()
- reads view model
- present(response)
- save(order)
- implements
- SQL via pg driver
Cómo leerlo:
- Cuatro paquetes UML, el más externo a la izquierda. Moverse a la derecha es moverse hacia dentro.
- Las flechas continuas son llamadas e imports. Tres cruzan una frontera hacia las entities. Las otras tres se quedan dentro de una capa:
present(response)ysave(order)llaman a las dos interfaces que pertenecen a la capa de use cases. - Las flechas discontinuas con triángulos huecos significan implementa. El presenter y el repositorio apuntan hacia dentro a esas interfaces mientras el control fluye hacia fuera.
- La línea de puntos es SQL a través del driver, y empieza en un adapter.
- Las notas y el bloque Markdown dicen dónde vive
main.ts, qué impone la entity y qué significa cada estilo de línea.
Dibujar diagramas de clean architecture con claridad
- Dibuja un use case, no los anillos. Traza una funcionalidad real con nombres de clase reales.
- Pon cada interfaz en la capa a la que pertenece.
OrderRepositoryestá junto aPlaceOrder, no junto a su implementación. - Usa dos estilos de flecha: continua para llamadas, discontinua con triángulo hueco para implementa, como en la realización de UML. El ejemplo de diagrama de clases UML cubre la notación.
- Dale a «hacia dentro» una sola dirección en la página. Una flecha que vuelve es una violación o una llamada en ejecución que hay que etiquetar.
- Etiqueta las flechas con el método y el modelo que cruza.
- Deja fuera los campos de los DTO, los internos del framework y el contenedor de DI.
Para el sistema que rodea esta vista a nivel de código, consulta el ejemplo de diagrama del modelo C4 y la guía para dibujar diagramas de arquitectura de software.
Constrúyelo en Archboard
- Inicia sesión con Google o GitHub y crea un diagrama.
- Añade cuatro nodos Paquete del pack UML en fila: Frameworks and drivers, Interface adapters, Use cases y Entities, con acentos azul, verde, rojo y ámbar.
- En Frameworks and drivers, añade un Componente del pack UML llamado Web framework y un nodo Postgres del pack Datos llamado PostgreSQL.
- En Interface adapters, añade dos nodos Componente, OrderPresenter y OrderController, y una Clase llamada PostgresOrderRepository.
- En Use cases, añade una Clase llamada PlaceOrder entre dos nodos Interfaz, PlaceOrderOutput y OrderRepository.
- En Entities, añade una Clase llamada Order con sus campos y métodos.
- Dibuja seis aristas continuas etiquetadas para las llamadas, dos aristas discontinuas con puntas de triángulo para implementa y una arista de puntos de PostgresOrderRepository a PostgreSQL.
- Añade un bloque Markdown del pack Básico con la regla y los estilos de línea, y notas de Texto del pack Básico para cada capa.
Un diagrama de flujo de Mermaid de tus capas se convierte en un primer borrador con la herramienta de Mermaid a diagrama editable. Free permite 15 nodos de diagrama por diagrama, así que traza un use case por diagrama.
El ejemplo usa 14 nodos de diagrama, dentro del plan Free.
Preguntas frecuentes
¿Cuáles son las cuatro capas de la clean architecture? Entities (reglas de negocio de la empresa), use cases (reglas de negocio de la aplicación), interface adapters (controladores, presenters y gateways) y frameworks and drivers (el framework web, la base de datos y otras herramientas). Martin dice que los círculos son esquemáticos: quizá necesites más de cuatro.
¿Qué es la regla de dependencia? Las dependencias del código fuente apuntan solo hacia dentro. El código de una capa interna nunca nombra nada declarado en una capa externa, así que los use cases no saben nada de controladores, SQL ni del framework web.
¿Cuál es la diferencia entre clean architecture y arquitectura hexagonal? Ambas mantienen las reglas de negocio en el centro y hacen que los adapters dependan de ellas. La arquitectura hexagonal solo separa el interior del exterior, mediante ports. La clean architecture además divide el interior en entities y use cases y nombra presenters y modelos de solicitud y respuesta.
¿Dónde va la interfaz del repositorio en la clean architecture? En la capa de use cases, junto al use case que la necesita. La implementación, con el SQL, pertenece a la capa de interface adapters, y el composition root la pasa al arrancar.
¿Es la clean architecture excesiva para proyectos pequeños? A menudo. Para servicios CRUD, prototipos y herramientas de corta vida, las interfaces y asignaciones extra cuestan más de lo que devuelven. Añade interfaces para las dependencias que esperas que cambien, y haz crecer las capas cuando aparezcan reglas de negocio reales.