Saltar al contenido

Diagrama de clean architecture: capas, regla de dependencia y un ejemplo completo

La clean architecture de Robert C. Martin como diagrama que puedes construir: las cuatro capas, la regla de dependencia y un ejemplo PlaceOrder con código y carpetas.

· Fundador y desarrollador

Publicado el

Cuatro paquetes UML en fila: frameworks and drivers, interface adapters, use cases y entities. El Web framework llama a OrderController, que llama a PlaceOrder, que crea Order. OrderPresenter y PostgresOrderRepository implementan interfaces de la capa de use cases.

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:

  • entities no importa nada de las otras capas.
  • use-cases solo importa entities: ni framework web, ni ORM, ni driver.
  • Los adapters importan hacia dentro, más la biblioteca que adaptan.
  • Solo main.ts lo 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:

  1. El framework web la enruta a OrderController.
  2. El controlador convierte el cuerpo en un PlaceOrderRequest, un objeto simple que pertenece a la capa de use cases, crea un OrderPresenter y llama a PlaceOrder.execute().
  3. PlaceOrder deja que Order.place() aplique las reglas de negocio, guarda a través de OrderRepository y pasa un PlaceOrderResponse a PlaceOrderOutput.
  4. OrderPresenter implementa esa interfaz y construye un view model: estado 201, una cabecera Location y un cuerpo JSON.
  5. 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, nunca Order, 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 Order atan 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

Cuatro paquetes UML en fila: frameworks and drivers, interface adapters, use cases y entities. El Web framework llama a OrderController, que llama a PlaceOrder, que crea Order. OrderPresenter y PostgresOrderRepository implementan interfaces de la capa de use cases.

20 componentes · 9 conexiones

Abrir en Archboard
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
Un use case, PlaceOrder, trazado por las cuatro capas de la clean architecture, la más externa a la izquierda. Ninguna flecha apunta hacia fuera: el presenter y el repositorio Postgres implementan interfaces de la capa de use cases; el control sale, las dependencias apuntan hacia dentro.

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) y save(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. OrderRepository está junto a PlaceOrder, 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

  1. Inicia sesión con Google o GitHub y crea un diagrama.
  2. 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.
  3. En Frameworks and drivers, añade un Componente del pack UML llamado Web framework y un nodo Postgres del pack Datos llamado PostgreSQL.
  4. En Interface adapters, añade dos nodos Componente, OrderPresenter y OrderController, y una Clase llamada PostgresOrderRepository.
  5. En Use cases, añade una Clase llamada PlaceOrder entre dos nodos Interfaz, PlaceOrderOutput y OrderRepository.
  6. En Entities, añade una Clase llamada Order con sus campos y métodos.
  7. 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.
  8. 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.

Empieza gratis — un puesto, sin tarjeta de crédito.

Artículos relacionados

← Volver al blog

Abre un lienzo en un minuto

Inicia sesión con Google o GitHub. Gratis para un asiento. Mejora el plan cuando se una el equipo.