Domain-Driven Design: Clean Architecture, cómo proteger el modelo de dominio mediante dependencias bien diseñadas.
Refactoriza la confirmación de pedidos desde un controlador acoplado hacia Order, caso de uso, puerto y adaptadores. Aprende la regla de dependencias, composición manual y pruebas sin infraestructura, con TypeScript y Express.
La confirmación de pedidos empieza como una ruta pequeña. Después incorpora comprobaciones de pago, reserva de existencias, estados y persistencia. Para probar una regla, el equipo necesita levantar Express y preparar una base de datos.
Cambiar un nombre de columna obliga a revisar lógica comercial. Ejecutar la misma operación desde otra entrada implica copiar parte del controlador.
Este es un contraejemplo didáctico; db representa un cliente SQL no implementado aquí:
Además de los controles omitidos, la ruta mezcla transporte, representación de datos y decisión comercial. Separarlos en archivos no bastará si las dependencias siguen apuntando a los detalles técnicos.
Vamos a refactorizar este ejemplo dentro del contexto Sales. Supondremos que las reglas ya se descubrieron y que el objetivo ahora es protegerlas.
DDD y Clean Architecture resuelven preguntas diferentes
DDD ayuda a representar conocimiento y reglas del negocio. Clean Architecture busca proteger políticas internas frente a detalles como transporte o almacenamiento.
Su Dependency Rule establece que las dependencias del código fuente apuntan hacia las políticas internas. Una capa interna no debe conocer tipos definidos por una externa. No se refiere simplemente al orden de llamadas durante la ejecución. Robert C. Martin: The Clean Architecture.
El modelo clásico distingue:
Entities: políticas de negocio generales.
Use Cases: comportamiento específico de la aplicación.
Interface Adapters: conversión entre representaciones.
Frameworks & Drivers: herramientas y mecanismos externos.
Podemos relacionarlos con Domain, Application, Presentation e Infrastructure, pero no son equivalencias exactas. Por ejemplo, adaptadores HTTP y de persistencia pertenecen conceptualmente a Interface Adapters, aunque un proyecto los organice en directorios distintos.
«Entities» de Clean Architecture tampoco equivale únicamente al patrón Entity de DDD. Lo importante aquí es qué conocimiento protegemos y quién puede depender de él.
Dibujar imports y llamadas por separado
Para nuestro ejemplo proponemos:
Dependencias de código:
Controlador HTTP ---> Caso de uso ---> Modelo Order
Adaptador memoria --> Puerto OrderRepository ---> Modelo Order
Composición --------> Implementaciones que conecta
Durante una petición, el caso de uso invocará el repositorio concreto. Sin embargo, su código conocerá solo un contrato interno.
Eso es inversión de dependencias: el comportamiento de aplicación no importa la implementación de almacenamiento; el adaptador implementa la capacidad que la aplicación necesita.
No hace falta un contenedor para lograrlo. Un constructor que recibe la dependencia puede ser suficiente.
Alcance y reglas del ejemplo
Para esta tienda ficticia acordamos que un pedido se puede confirmar cuando:
No está cancelado.
Contiene al menos una línea.
Sales dispone de confirmación de pago y reserva correspondientes.
Una confirmación repetida de un pedido ya confirmado no cambia su estado.
Los indicadores de pago y reserva son hechos locales previamente verificados. No se aceptan desde el cuerpo HTTP ni demuestran por sí solos una integración completa.
No implementaremos cobros o reservas. Si esos hechos proceden de otros contextos, deben obtenerse mediante contratos y traducciones del consumidor, sin importar sus entidades.
Usamos Order como raíz del agregado del ejemplo, sin reconstruir todo su ciclo de vida. restore presupone una representación interna válida en sus tipos; el adaptador real debe validar datos leídos de almacenamiento.
No hay Request, filas SQL ni códigos HTTP. OrderRuleError expresa que no puede cumplirse una regla, no que deba enviarse necesariamente un 409.
La copia del snapshot evita entregar una referencia mutable al estado. No pretende ser un serializador universal del dominio ni el contrato público de la API.
Definir el puerto donde se necesita
La aplicación necesita cargar y guardar Order. Elegimos situar su contrato en Application; otros diseños pueden ubicarlo en Domain cuando responda a sus responsabilidades. La condición importante es que no viva dentro del adaptador técnico.
El contrato de guardado exige comparar la revisión esperada y aplicar la escritura atómicamente. false representa un conflicto que impidió guardar.
Es una decisión acotada para mostrar concurrencia sin un ORM. No significa que toda aplicación deba utilizar exactamente esta interfaz.
Evita definir un puerto que devuelva SqlRow, un objeto del framework de persistencia o un Response HTTP. La firma también puede filtrar una dependencia aunque el import esté marcado como type.
El caso de uso coordina sin conocer el almacenamiento
El caso de uso conoce el contrato y el modelo. No hace new SqlOrderRepository(), no selecciona conexiones y no devuelve una entidad para serializarla directamente al cliente.
La comprobación de revisión evita perder cambios si la implementación del puerto cumple su garantía. No sustituye todas las reglas de consistencia del proceso.
Dos solicitudes concurrentes podrían producir un éxito y un conflicto; un intento posterior puede observar el pedido confirmado. No prometemos que cualquier repetición concurrente devuelva éxito sin revisar el resultado.
Un adaptador de persistencia sin base de datos
Para concentrarnos en las dependencias, implementamos almacenamiento en memoria. En sales/infrastructure/in-memory-order-repository.ts:
El adaptador conoce el puerto interno y cómo reconstruir el modelo. La aplicación no conoce este Map.
La comparación y escritura no contienen una espera intermedia y sirven para una única instancia en memoria. No es almacenamiento durable ni una solución de concurrencia para varios procesos.
Un adaptador SQL tendría que realizar su comparación y actualización de forma atómica, verificar el resultado y traducir errores y representaciones. No basta con leer una revisión y actualizar después sin protección.
Cambiarlo implica trabajo real de implementación, pruebas y operación. La arquitectura evita que ese detalle redefina las reglas de Order; no vuelve gratuita una migración.
El controlador transforma HTTP a una entrada simple
Utilizamos Express 5 para la entrada. Los errores de handlers asíncronos se propagan al middleware de errores; ese comportamiento debe considerarse si se utiliza otra versión o framework. Express: manejo de errores.
authorize es una dependencia obligatoria que debe autenticar y comprobar el permiso sobre ese pedido. No se proporciona una implementación ficticia para producción. Si la entrada utiliza cookies, también debe aplicar la protección correspondiente frente a solicitudes cruzadas.
Este tutorial separa el control HTTP para limitar el ejemplo. Si el caso de uso se expone por otras entradas, cada una debe aplicar la política de autorización necesaria; una política de aplicación común puede situarse dentro de Application mediante sus propios contratos.
El cliente no puede marcar pago o reserva como válidos enviando booleanos: la ruta no los acepta como entrada.
Composición manual: conocer detalles en el lugar adecuado
La raíz de composición conoce implementaciones concretas porque su responsabilidad es conectarlas. Eso no autoriza al dominio a importarlas de vuelta.
El ejemplo no arranca un servidor ni incluye autenticación o configuración del proyecto. Presupone TypeScript, Node.js, Express 5 y sus tipos disponibles, con compilación ESM adecuada. No es un despliegue listo para producción.
Los archivos mostrados contienen las piezas del recorrido didáctico. No necesitamos Inversify para comprender cómo se conecta una implementación a un contrato.
Recorrer la operación
Con autorización válida, una petición POST /orders/order-100/confirm carga una copia del pedido pendiente, aplica su regla y guarda la nueva revisión.
La respuesta es:
{"orderId":"order-100","status":"CONFIRMED"}
Una lectura posterior del repositorio observa el nuevo estado. En memoria se pierde al reiniciar; el límite se explica, no se oculta con el nombre «persistencia».
Probar sin Express ni base real
Estas pruebas propuestas usan APIs integradas de Node.js y presuponen archivos compilados a JavaScript ESM. No se han ejecutado como parte del artículo.
Son pruebas del caso de uso con un adaptador controlado. No cubren el transporte, autorización o una base real.
También deben comprobarse cancelación, repetición, pedido inexistente y conflicto de revisión. Un adaptador real necesita su propia verificación de contrato y concurrencia.
La independencia técnica hace posible probar las decisiones sin levantar toda la aplicación. No significa que las pruebas de integración dejen de ser necesarias.
Detectar violaciones antes de que crezcan
Revisa imports y firmas, no solamente carpetas.
Señales concretas:
Order importa Express o un cliente de base de datos.
ConfirmOrderUseCase crea directamente el adaptador SQL.
OrderRepository devuelve filas específicas de un driver.
El controlador decide que un pedido sin pago puede confirmarse.
Un caso de uso recibe Request o modifica Response.
La composición se importa desde el dominio.
TypeScript elimina interfaces al compilar, pero eso no impide que filtren conceptos técnicos durante el diseño. import type no convierte una dependencia incorrecta en correcta.
Las comprobaciones de arquitectura pueden automatizar restricciones de imports. Las revisiones y pruebas siguen siendo necesarias para detectar decisiones mal ubicadas o contratos poco expresivos.
Límites entre contextos y sobreingeniería
Clean Architecture no autoriza a importar el agregado Payments dentro de Sales por ser «dominio». Proteger capas internas y respetar límites entre contextos son condiciones distintas que deben mantenerse juntas.
Si Sales necesita evidencia de otro contexto, su aplicación puede definir un puerto y recibir una traducción mediante ACL HTTP. La llamada no forma parte automáticamente de su transacción local.
Tampoco hace falta repetir todas estas piezas para una operación trivial. Una consulta administrativa pequeña puede utilizar una representación de lectura sin reconstruir un agregado que no aporta comportamiento.
En una plataforma educativa, una regla que permite completar una actividad merece separación de sus detalles de pantalla o almacenamiento. Un listado estático de etiquetas quizá solo necesite una función sencilla.
En reservas, una política de solapamientos puede probarse sin framework. Una exportación acotada de texto puede no necesitar una interfaz para cada transformación.
El nivel de estructura debe responder al valor y complejidad reales. Multiplicar clases delegadoras sin una responsabilidad propia puede hacer más difícil seguir el comportamiento.
Dar a la IA reglas comprobables
Para Cursor, Codex o Claude, especifica qué dependencias están permitidas.
Para implementar:
Trabaja en Sales con Domain sin dependencias de framework. ConfirmOrderUseCase recibe OrderRepository definido en Application. El adaptador implementa ese puerto y la composición se hace manualmente. No importes entidades de otros contextos ni aceptes evidencia financiera desde HTTP.
Para revisar:
Revisa imports, tipos de entrada y salida y creación de dependencias. Señala dónde una política interna conoce Express, SQL o DTOs externos. Propón cambios mínimos y explica qué comportamiento protegen. No evalúes la arquitectura solo por los nombres de carpetas.
Para probar:
Diseña pruebas del caso de uso sin servidor ni base real para confirmación, rechazo y conflicto. Separa esas pruebas de las necesarias para el adaptador de persistencia y autorización. No afirmes que el doble en memoria demuestra durabilidad o concurrencia entre procesos.
Una persona debe confirmar reglas y garantías. Un agente puede respetar el árbol de carpetas y seguir colocando la decisión comercial en el lugar equivocado.
Ejercicios
1. La interfaz colocada afuera
OrderRepository vive junto al cliente SQL y Application la importa desde allí. ¿Qué revisarías?
2. El tipo HTTP conveniente
El caso de uso acepta Request para leer un encabezado y ahorrar un objeto de entrada. ¿Qué dependencia introduce?
3. Más carpetas, misma decisión
El controlador llama a un servicio que confirma cualquier pedido y luego comprueba el pago antes de enviar la respuesta. ¿Se ha protegido la regla?
Soluciones razonadas
1. Quién define la necesidad. Si el puerto pertenece a la aplicación, debe poder existir sin conocer el adaptador técnico. Mover el contrato y eliminar tipos del driver puede invertir correctamente la dependencia.
2. El caso de uso queda vinculado al transporte y a sus tipos. La entrada debe traducir lo necesario a una representación propia, o utilizar un contrato de aplicación para la capacidad requerida.
3. No. La regla se comprueba después y sigue fuera del comportamiento protegido. La confirmación no debe persistirse antes de evaluar sus condiciones. Cambiar nombres de archivos no corrige el orden ni la responsabilidad.
Proteger políticas mediante decisiones visibles
Clean Architecture complementa al modelado cuando las reglas pueden entenderse y probarse sin depender de cómo entra una petición o dónde se guardan los datos.
La evidencia está en las dependencias, los contratos y las pruebas, no en reproducir un diagrama de círculos.
Empieza por una regla relevante y un límite claro. Añade las piezas que permiten sostenerlos, conservando explícitas las garantías que el ejemplo todavía no implementa.
Integra la colección construyendo una API de pedidos con TypeScript, Express, Zod y SQLite. Incluye los archivos del proyecto, reglas del dominio, eventos, control de versiones, pruebas con Vitest y comandos para ejecutarla y verificarla localmente.
Aprende a orientar agentes de IA con reglas del negocio, límites de contexto y criterios verificables. Un caso en TypeScript muestra cómo preparar instrucciones, proteger Order, diseñar pruebas y revisar cambios sin delegar el criterio arquitectónico.
Compara un monolito modular con una separación de Sales, Payments e Inventory. Diseña contratos en TypeScript y Express y comprende fallos parciales, consistencia eventual, idempotencia, Outbox y Sagas sin introducir infraestructura compleja.