Software Architecture
Clean Architecture
Explica Clean Architecture como una regla de dependencias que protege dominio y casos de uso frente a HTTP, persistencia, frameworks y proveedores.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Software Architecture
Explica Clean Architecture como una regla de dependencias que protege dominio y casos de uso frente a HTTP, persistencia, frameworks y proveedores.
Clean Architecture protege las políticas y casos de uso del sistema haciendo que las dependencias del código apunten desde mecanismos externos hacia conceptos más estables. No es una plantilla de carpetas ni una obligación de crear capas para todo.
Clean Architecture pertenece a una familia de enfoques —junto con Hexagonal y Onion— que separan reglas de negocio de detalles como HTTP, bases de datos, frameworks y proveedores.
frameworks y drivers
↓
interface adapters
↓
aplicación / casos de uso
↓
dominio / reglas esencialesLa regla central es la dirección de dependencia: una parte interior no conoce tipos ni mecanismos de una parte exterior.
En una aplicación organizada directamente alrededor del framework, los casos de uso suelen depender de controllers, ORM y SDKs. Esto hace que:
Clean Architecture intenta mantener la lógica importante utilizable aunque cambien mecanismos externos.
Contiene conceptos, invariantes y comportamiento que existen por el problema de negocio. Un Order puede decidir si puede confirmarse y proteger transiciones válidas.
Contiene casos de uso. Coordina una intención, obtiene datos, invoca reglas, delimita transacciones y devuelve resultados de aplicación.
Traducen entre el lenguaje externo y el interno: controllers, presenters, repositories, gateways y mappers.
HTTP, base de datos, broker, filesystem, runtime, proveedores y librerías concretas.
Estas capas son conceptuales. Una aplicación pequeña puede representarlas con pocos archivos. El objetivo es proteger dependencias, no reproducir un diagrama.
export interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
}
export class ConfirmOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(command: ConfirmOrderCommand): Promise<ConfirmOrderResult> {
const order = await this.orders.findById(command.orderId);
if (!order) return { type: "not-found" };
if (!order.canBeConfirmed()) return { type: "invalid-state" };
order.confirm();
await this.orders.save(order);
return { type: "confirmed", orderId: order.id };
}
}El caso de uso define el contrato que necesita. Un adapter PostgreSQL lo implementa. En ejecución, el caso de uso llama al adapter; en compilación, el adapter depende del contrato interior.
HTTP request
→ controller valida forma y autenticación inicial
→ crea ConfirmOrderCommand
→ caso de uso carga Order
→ dominio verifica reglas
→ repository persiste
→ presenter transforma resultado
→ HTTP responseCada límite traduce un modelo. El controller no debería contener la regla de confirmación; el dominio no debería conocer códigos HTTP; el repository no debería decidir autorización de negocio.
No es obligatorio tener una clase distinta para request, command, entidad, fila y response. La separación aporta cuando existen responsabilidades o ciclos de vida diferentes.
HTTP DTO
→ contrato público y validación de forma
Command
→ intención de aplicación
Domain model
→ reglas e invariantes
Persistence model
→ representación del motor
Response DTO
→ contrato de salidaReutilizar un objeto ORM en todas las capas reduce mapeo, pero acopla contratos y reglas al esquema. Duplicar modelos idénticos sin razón crea ceremonia. La decisión depende de cuánto conocimiento necesitas aislar.
El caso de uso suele definir la unidad lógica de trabajo. La infraestructura implementa el mecanismo.
interface UnitOfWork {
execute<T>(work: () => Promise<T>): Promise<T>;
}El dominio no necesita conocer conexiones. Sin embargo, una abstracción transaccional demasiado genérica puede ocultar locks, aislamiento y límites reales. La arquitectura debe conservar semántica suficiente para diseñar concurrencia.
Los adaptadores traducen errores externos:
unique constraint
→ OrderAlreadyExists
timeout de proveedor
→ PaymentResultUnknownEl caso de uso decide qué hacer con esos resultados. No todos los errores deben convertirse en una excepción genérica.
Controller
→ valida tenant y payload
ReserveStock use case
→ carga inventario
→ aplica política de reserva
→ persiste movimiento y nueva disponibilidad
Postgres adapter
→ usa update atómico, locks y transacción
Presenter
→ devuelve reserved, insufficient-stock o conflictClean Architecture organiza dependencias; no reemplaza el conocimiento del dominio ni las decisiones distribuidas.
Pruebas rápidas de invariantes y transiciones.
Fakes para puertos y comprobación de resultados, efectos y errores.
Pruebas de integración con DB, broker o proveedor.
Pocas pruebas end-to-end que validen ensamblaje y contratos.
Un fake no demuestra que PostgreSQL maneje concurrencia. Una prueba end-to-end no sustituye pruebas precisas de reglas.
Enfatiza círculos conceptuales, políticas y regla de dependencia.
Enfatiza puertos y adaptadores alrededor de un núcleo y la simetría entre entradas y salidas.
Enfatiza capas concéntricas alrededor del dominio.
Organiza responsabilidades por niveles, pero puede permitir que negocio dependa de persistencia si no se aplica inversión.
Comparten ideas, pero no son etiquetas intercambiables exactas.
Crear dominio, casos de uso y puertos para cada tabla puede añadir navegación sin beneficio. Una estructura más ligera puede ser correcta.
No necesitas ocultar cada API. Protege únicamente dependencias cuyo cambio, prueba o semántica importa.
Las consultas de lectura pueden usar proyecciones directas sin reconstruir agregados, siempre que no modifiquen invariantes.
Cada módulo puede aplicar Clean internamente. Evita una capa global de domain, application e infrastructure que mezcle todos los contextos.
Arquitectura hexagonal, ports and adapters observa la misma separación desde las capacidades de entrada y salida del núcleo.