Domain-Driven Design: qué es, cómo funciona y cómo te ayuda a programar con IA
Aprende a traducir reglas de negocio en software con Domain-Driven Design. Recorre sus conceptos fundamentales mediante ejemplos en TypeScript y descubre cómo usarlos para orientar y revisar código generado por IA, más allá de que compile o responda correctamente una petición.
Una academia abre veinte plazas para un taller. Pides a una herramienta de inteligencia artificial que implemente las inscripciones: genera un endpoint, valida el JSON y guarda registros. La demostración funciona. Sin embargo, cuando queda una plaza, dos estudiantes se inscriben casi al mismo tiempo y ambos reciben una confirmación.
El código puede estar bien escrito y aun así incumplir lo que la academia necesita. Validar que studentId sea una cadena no responde quién controla el cupo ni qué debe suceder cuando dos solicitudes compiten por él.
Domain-Driven Design, o DDD, ayuda a convertir esas preguntas en un modelo explícito: conceptos, reglas y límites que guían la implementación. Para entenderlo, empezaremos por el problema del negocio y terminaremos con un ejemplo comprobable en TypeScript.
Qué es DDD y qué problema resuelve
DDD es un enfoque de desarrollo que centra el diseño en el conocimiento del Domain y en la colaboración con quienes lo conocen. El Domain es el ámbito de actividad que el software ayuda a resolver: formación, comercio o gestión de reservas, por ejemplo. Un Domain Model es una representación que selecciona los conceptos y reglas relevantes para ese propósito; no intenta copiar toda la realidad. Esta es la orientación que presenta Eric Evans en su referencia de DDD.
En una academia, hablar con coordinación puede revelar que «curso» significa el contenido, mientras que «edición» identifica un grupo con fechas y plazas. Si guardas el cupo directamente en Course, quizá impidas ofrecer dos ediciones simultáneas con capacidades diferentes.
La decisión de modelado cambia el resultado: CourseEdition controla las inscripciones de una edición concreta. Ahora puedes abrir una edición de veinte plazas y otra de treinta sin mezclar sus participantes.
DDD busca reducir esa distancia entre lo que las personas quieren expresar y lo que el programa permite hacer. No garantiza ausencia de errores. Hace que ciertas decisiones sean visibles, discutibles y comprobables.
Conviene distinguir dos escalas. El Strategic Design organiza responsabilidades, modelos y relaciones entre áreas. El Tactical Design expresa reglas dentro de un modelo mediante herramientas como Entities y Aggregates. Empezar por las clases sin aclarar los límites deja sin resolver parte del problema.
Primero entiende las palabras y las responsabilidades
Domain y Subdomains: dónde merece la pena invertir
Un Subdomain es una parte del problema general. En nuestra academia podríamos distinguir planificación de ediciones, aprendizaje, facturación y comunicación.
No todas requieren el mismo esfuerzo. Si la academia compite por cómo adapta los ejercicios a cada estudiante, esa capacidad puede ser su núcleo diferenciador. Enviar correos podría resolverse con un servicio existente. La clasificación depende del negocio: para una empresa de correo, entregar mensajes sería central.
Como criterio práctico, identifica qué comportamiento diferencia al producto, qué capacidades propias lo apoyan y qué necesidades son comunes a muchas organizaciones. Así evitas dedicar semanas a un modelo sofisticado para una función secundaria mientras las reglas importantes siguen dispersas.
Ubiquitous Language: nombres que también sirven para conversar
El Ubiquitous Language es el vocabulario compartido que conecta conversaciones, documentación y código dentro de un modelo. Evans lo incluye como pieza fundamental de DDD en su referencia.
Para el taller, acordamos lo siguiente:
Una edición es una convocatoria concreta de un curso.
Inscribir consume una plaza inmediatamente.
Una persona no puede inscribirse dos veces en la misma edición.
En este ejemplo no existen reservas temporales ni lista de espera.
La última precisión importa. Si coordinación llama «reserva» a una plaza retenida durante quince minutos, implementar enroll() con ocupación permanente sería incorrecto. Antes de pedir código a la IA, necesitas resolver esa diferencia.
El vocabulario se revisa cuando aparecen casos nuevos. Si el equipo descubre que reservar e inscribir son acciones distintas, el modelo debe reflejarlo; un glosario que nadie contrasta con el comportamiento aporta poco.
Bounded Contexts: una palabra puede tener significados distintos
Un Bounded Context marca dónde un modelo y su lenguaje tienen validez. No obliga a desplegar un microservicio por Bounded Context: es, ante todo, un límite de significado y responsabilidad. Martin Fowler explica cómo separar modelos relacionados mediante Bounded Contexts.
En aprendizaje, «estudiante» puede tener progreso y entregas. En facturación, «cliente» puede tener datos fiscales y obligaciones de pago. Aunque ambos hagan referencia a la misma persona, no necesitas una clase universal con todos esos atributos.
El Subdomain describe una parte del problema; el Bounded Context delimita un modelo de solución. Son conceptos relacionados, pero no equivalentes por definición. Puedes revisar su correspondencia a medida que entiendes mejor el sistema.
Entities, Value Objects y Aggregates
Una Entity se distingue por su identidad a lo largo del tiempo. Una edición conserva su identificador aunque cambie de horario. Dos ediciones con las mismas fechas no son necesariamente la misma.
Un Value Object representa algo definido por sus atributos. Dos cantidades de diez euros son equivalentes si coinciden importe y moneda. Habitualmente lo haces inmutable para conservar ese significado.
Un Aggregate agrupa elementos que deben mantener reglas coherentes; su Aggregate Root controla las modificaciones externas. Una Invariant es una condición que debe mantenerse tras cada operación válida. Estas distinciones forman parte de los patrones de Tactical Design descritos por Evans.
En el taller elegiremos CourseEdition como Aggregate Root. Su Invariant será «el número de inscritos nunca supera la capacidad». Exponer un arreglo público para que cualquiera añada participantes permitiría saltarse esa regla. Ofrecer enroll(studentId) concentra la decisión.
Un Aggregate puede contener solo una Entity. Tampoco debes incluir todos los datos relacionados: no cargaremos el historial académico ni las facturas de cada inscrito. El límite se elige por las reglas que necesitan consistencia conjunta, no por las relaciones que puedas dibujar entre tablas.
Ejemplo principal: inscribir sin superar el cupo
De una comprobación aislada a una regla protegida
Esta solución problemática parece razonable. Es pseudocódigo parcial; database representa una API de persistencia ficticia:
if (await database.countEnrollments(editionId) < capacity) {
await database.insertEnrollment(editionId, studentId);
}
Dos solicitudes pueden contar diecinueve inscritos antes de que ninguna guarde. Cada comprobación es correcta por separado; la combinación admite veintiuno.
Necesitamos dos decisiones complementarias: el modelo rechaza inscripciones inválidas y la persistencia impide guardar cambios basados en un estado desactualizado. Una clase, por sí sola, no coordina procesos concurrentes.
Contrato y simplificaciones del ejemplo
Representaremos esta petición:
POST /editions/typescript-octubre/enrollments
Content-Type: application/json
{}
La identidad procede de una sesión ya autenticada, no del JSON. La respuesta satisfactoria será 201 con la edición y el estudiante. Una inscripción duplicada, una edición llena o un conflicto de versión devolverán 409; una edición inexistente, 404.
El siguiente bloque es un programa autocontenido. Guárdalo como inscripciones.ts. Usa únicamente módulos incluidos en Node.js. Puedes ejecutarlo con node inscripciones.ts en Node.js 24.21.0, la versión usada para comprobarlo. Node puede ejecutar esta sintaxis eliminando los tipos, pero no realiza comprobación de tipos durante esa ejecución.
Para centrarnos en DDD, el adaptador postEnrollment recibe el cuerpo ya decodificado y una sesión confiable; no abre un servidor HTTP. El Repository guarda copias en memoria y pierde los datos al terminar. Simula control de concurrencia en un único proceso, sin ofrecer persistencia durable ni coordinación entre servidores.
Primero, el adaptador valida la forma del cuerpo. Rechaza campos inesperados: el cliente no puede elegir otra identidad. En un servidor real también comprobarías autenticación, permisos, parámetros de ruta y límites de la petición.
Después, enrollStudent ejecuta el Use Case, la operación que la aplicación ofrece. Recupera la edición y coordina el guardado. No calcula por su cuenta si quedan plazas: delega esa regla en CourseEdition.
La Entity encapsula el estado y permite modificarlo mediante enroll. Cambiar el cupo admitido o incorporar un cierre de inscripciones tendría un lugar concreto en el modelo. La validación del transporte y las reglas del Domain cumplen responsabilidades diferentes.
El Repository ofrece recuperar y guardar Aggregates sin que el Use Case conozca una base de datos específica. Aquí su contrato incluye una condición relevante: guardar solo si coincide la versión leída.
En la prueba concurrente, ambas operaciones pueden trabajar con la versión cero. La primera guarda la versión uno; la segunda encuentra una versión distinta y falla. El resultado observable es una confirmación y un conflicto, con una sola plaza ocupada.
En una base de datos, la comparación y la escritura también deben ser atómicas: por ejemplo, mediante una actualización condicionada por versión, comprobando las filas afectadas y guardando todos los cambios del Aggregate en una transacción local. Separar esas comprobaciones volvería a introducir la carrera. Si decides reintentar un conflicto, debes cargar el estado actual y ejecutar de nuevo las reglas.
Las aserciones protegen comportamientos del negocio, además de la respuesta: rechazan duplicados, impiden superar la capacidad y verifican el estado finalmente guardado. El programa se ejecutó con Node.js 24.21.0 y completó esas aserciones; no se realizó una compilación con el comprobador de tipos de TypeScript. Esta demostración no sustituye una prueba concurrente contra el motor de persistencia real.
Nuestra elección tiene un coste: cargar todos los inscritos puede ser razonable para un taller pequeño y resultar inadecuado para un evento masivo. En ese caso habría que reconsiderar cómo representar la asignación de plazas y proteger su consistencia. DDD exige revisar el modelo, no conservar esta implementación a cualquier escala.
Segundo ejemplo: dinero en un comercio
Ahora cambia la decisión. Una tienda recibe precios desde una integración. La solución problemática representa todo como number y termina sumando diez euros con diez dólares como si fueran veinte unidades de una misma moneda.
Aquí necesitamos un Value Object que haga explícitas las unidades. Este fragmento didáctico es independiente del programa anterior y limita deliberadamente el ejemplo a euros y dólares con dos decimales:
Representar unidades menores evita ciertos problemas de operar con decimales binarios, pero no resuelve conversiones, impuestos ni redondeos. Esas reglas requieren decisiones adicionales. Además, los tipos de TypeScript no validan datos recibidos por HTTP: el adaptador debe comprobar la moneda en tiempo de ejecución.
Este ejemplo enseña a reconocer cuándo un dato merece un significado propio. No necesitamos un identificador para cada importe; necesitamos igualdad por valor y operaciones compatibles con sus unidades.
Tercer ejemplo: traducir una integración logística
Una tienda recibe del transportista el estado DELIVERED. Al principio, alguien lo transforma directamente en order.completed = true. El problema aparece cuando el negocio distingue entrega física, aceptación del cliente y cierre comercial.
La decisión es introducir una Anticorruption Layer, o ACL: un adaptador que traduce el modelo externo al lenguaje que necesita el consumidor. Evans describe este patrón para proteger el modelo propio frente a otros modelos.
En nuestro escenario, la ACL valida la respuesta del transportista y traduce DELIVERED a «entrega física confirmada». El Use Case registra ese hecho; una regla comercial separada decide si corresponde cerrar el pedido. El resultado cambia: recibir el paquete ya no salta automáticamente la aceptación pendiente.
El contrato del consumidor podría ser este fragmento parcial, sin implementación HTTP:
El consumidor define lo que necesita saber. Su adaptador implementa el contrato, valida el JSON externo y distingue «no entregado» de «no pude consultar». Un fallo de red no debe convertirse silenciosamente en un hecho del negocio.
Esto también aclara un límite: consultar por HTTP y guardar en tu base de datos no constituye una transacción compartida. Si debes coordinar acciones entre sistemas, necesitas acordar qué ocurre ante fallos parciales, repeticiones y datos que cambian entre consultas. La ACL traduce; no elimina esos problemas.
Código de ejemplo basado en un caso real
En un caso real de publicación de comentarios, el código de ejemplo permite distinguir dos responsabilidades: validar el contenido y comprobar las condiciones necesarias para publicarlo. Un Value Object puede normalizar el texto y rechazar un comentario vacío; el Use Case coordina las comprobaciones y el guardado.
Si una comprobación depende de otro Bounded Context, una ACL puede consultar su API HTTP y validar la respuesta contra un contrato definido por el consumidor. Así, cada Bounded Context conserva su modelo sin importar implementaciones del otro. Esta es una decisión de integración del ejemplo, no una obligación universal de DDD. La consulta HTTP y el guardado local no constituyen una transacción compartida.
Los ejemplos de este artículo son didácticos: simplifican los casos para explicar las decisiones de diseño.
Qué cambia cuando programas con inteligencia artificial
La relación con la IA es una aplicación práctica de estas ideas: un modelo explícito te da criterios para solicitar y revisar cambios. No supone que DDD haga que una herramienta entienda automáticamente tu negocio.
«Implementa inscripciones» deja abiertas preguntas que la IA puede resolver mediante suposiciones: permitir duplicados, convertir reservas en inscripciones o compartir una Entity entre Bounded Contexts. Un contrato más preciso reduce esa ambigüedad:
Describe el vocabulario y las reglas con ejemplos y contraejemplos.
Identifica qué Bounded Context decide cada cosa y qué datos necesita de otros.
Solicita una implementación acotada y comprobaciones observables.
Revisa el comportamiento con alguien que conozca el Domain.
Una prueba generada por la misma herramienta puede confirmar una suposición equivocada. Si tanto implementación como prueba consideran válida una segunda inscripción, ambas estarán de acuerdo y el negocio seguirá mal representado.
Estos prompts sirven como punto de partida. Completa el contexto con información real y comparte únicamente material autorizado.
Para analizar antes de implementar
Trabajamos en una academia. Curso es contenido; edición es una convocatoria con capacidad propia. Inscribir ocupa una plaza y no admite duplicados. No existen reservas temporales. Analiza los casos «última plaza», «dos solicitudes simultáneas» y «misma persona dos veces». Devuelve reglas, términos ambiguos y una propuesta de límite del Aggregate, sin escribir código. Separa hechos de supuestos. Una persona de coordinación validará especialmente el significado de inscripción y las excepciones.
Para implementar con límites claros
Implementa el Use Case InscribirEstudiante en TypeScript para Node.js. La identidad llega de una sesión confiable; el cuerpo no acepta studentId. CourseEdition protege capacidad y duplicados. El Repository debe detectar escrituras basadas en una versión antigua. No añadas frameworks, dependencias ni importaciones entre Bounded Contexts. Entrega código y pruebas de duplicado, edición llena y dos solicitudes para una plaza. Explica qué garantía depende de la base de datos. Una persona revisará la atomicidad del adaptador real y las reglas de autorización.
Para revisar sin confundir compilación con corrección
Revisa este cambio contra las reglas y contratos adjuntos. Busca vías para modificar el Aggregate sin sus métodos, confusiones entre reserva e inscripción, contratos externos sin validar e hipótesis de transacción compartida por HTTP. No cambies archivos. Devuelve hallazgos con evidencia y un escenario reproducible; marca las dudas como dudas. Una persona contrastará los escenarios con negocio y ejecutará las pruebas en el entorno correspondiente.
Cuándo basta una solución más simple
Un catálogo interno de aulas quizá solo necesite crear, consultar, modificar y eliminar registros: operaciones habitualmente llamadas CRUD. Si sus reglas se limitan a un nombre obligatorio y un código único, un servicio pequeño con validación y una restricción de unicidad en la base de datos puede ser suficiente.
Puedes conservar nombres claros y responsabilidades separadas sin crear un Aggregate, una Factory y varios servicios para cada tabla. DDD no exige microservicios, una carpeta determinada, CQRS ni almacenar todos los cambios como eventos. La visión general de Fowler destaca la importancia de Strategic Design y su aplicabilidad más allá de un único enfoque de programación.
El coste real incluye conversaciones, revisión de conceptos, mantenimiento de límites y aprendizaje del equipo. Merece la pena cuando evita errores importantes o facilita cambios frecuentes en reglas complejas. Si las abstracciones añaden pasos sin representar decisiones, conviene simplificarlas.
Tres ejercicios para aplicar el criterio
1. Una plaza reservada durante quince minutos
La academia quiere retener una plaza mientras se completa un pago. Decide qué cambiarías antes de programar.
Solución razonada: distingue reserva temporal de inscripción confirmada. Aclara cuándo expira, si una reserva consume capacidad y qué ocurre si el pago llega tarde. Si ambas ocupan plazas, la Invariant debe considerar reservas vigentes e inscripciones. Inyectar un reloj controlable permite comprobar la expiración sin esperar quince minutos. Una confirmación de pago repetida tampoco debería consumir otra plaza. Añadir únicamente un campo paid deja esas decisiones pendientes.
2. Una dirección nueva después del envío
Un cliente cambia su dirección de perfil después de despachar un pedido. ¿Debe cambiar también la dirección del envío?
Solución razonada: si logística necesita conservar dónde se despachó, guarda una copia del valor acordado al preparar el envío. La dirección del perfil puede cambiar sin reescribir ese hecho. Una corrección posterior requiere una operación explícita con sus propias reglas; no una actualización automática de todas las referencias.
3. El transportista no responde
La consulta de entrega agota su tiempo de espera. La IA propone devolver physicallyDelivered: false. ¿Lo aceptarías?
Solución razonada: no para el contrato descrito. «Desconocido por fallo de consulta» no equivale a «no entregado». Mantén la distinción mediante un error o un resultado explícito de indisponibilidad, y acuerda si el Use Case debe esperar, reintentar o dejar una revisión pendiente. Comprueba que un fallo de integración no cierre ni revierta un pedido por accidente.
Cómo reconocer dónde aplicar DDD
DDD resulta especialmente útil cuando las mismas palabras causan desacuerdos, varias operaciones deben proteger una regla común o una integración empieza a imponer su vocabulario al resto del sistema. También cuando cada cambio exige buscar condiciones duplicadas en controladores y servicios.
Empieza por una operación concreta: escribe sus reglas con quien conoce el negocio, identifica los términos y comprueba los casos que podrían romperla. Introduce Entities, Value Objects y Aggregates donde hagan explícitas decisiones necesarias. Contrasta después el modelo con la persistencia y las integraciones: una regla expresada en código todavía necesita mecanismos que la sostengan al ejecutarse.
Al trabajar con IA, ese modelo sirve para dirigir la implementación y discutir sus resultados. Que el código compile es una comprobación técnica; que permita exactamente las operaciones acordadas exige comprender el Domain y aportar evidencia de comportamiento.
Referencias para profundizar
Además de las fuentes enlazadas, se consultó la introducción del ejemplar local de Domain Driven Design Quickly, publicado por InfoQ, elaborado principalmente por Abel Avram con Floyd Marinescu como editor. Es una puerta de entrada a las ideas de Evans. Los escenarios y decisiones de implementación de este artículo son didácticos y deben adaptarse a las reglas de cada proyecto.
Aprende a distribuir responsabilidades entre Presentation, Application, Domain e Infrastructure. Recorre un endpoint completo con TypeScript y Express, distingue llamadas de dependencias y utiliza reglas claras para revisar tanto tu código como el generado por IA.
Descubre cómo un Domain Model guía las decisiones del código y en qué se diferencia de un Data Model. Con ejemplos en TypeScript, aprende a representar reglas, reconocer la responsabilidad de los patrones de DDD y dar a la IA criterios concretos para implementar y revisar cambios.
Construye un lenguaje compartido que permita tomar decisiones. Exploraremos reservas, logística y educación para detectar ambigüedades, escribir ejemplos precisos y hacer evolucionar nombres, contratos y pruebas con ayuda de la IA.