Por qué el CRUD común mata tu proyecto y cómo DDD puede solucionarlo
\n\nCada desarrollador se ha enfrentado al menos una vez a la situación en la que un proyecto aparentemente simple se convierte en un "pantano" de servicios, controladores y lógica de negocio dispersa por todo el código. Empiezas escribiendo UserService, luego OrderService, y seis meses después en OrderService hay lógica para enviar correos electrónicos, calcular descuentos y verificar el saldo del usuario. ¿Te suena familiar?
El problema es que pensamos demasiado pronto en bases de datos, frameworks y APIs REST. Nos olvidamos de lo principal: el dominio (domain). Domain-Driven Design (DDD) propone un enfoque radicalmente diferente: primero modelamos los procesos de negocio en el lenguaje de los expertos, y solo después el código.
\n\nEn este artículo, analizaremos cómo aplicar Domain-Driven Design en TypeScript con ejemplos reales. Verás cómo construir una arquitectura limpia donde las reglas de negocio viven separadas de la infraestructura y el código es fácil de probar y modificar.
\n\nFundamentos de DDD: lenguaje, contextos y modelo
\n\nUbiquitous Language (Lenguaje ubicuo)
\nEl primer principio de DDD es un lenguaje común, comprensible tanto para desarrolladores como para expertos de negocio. Si el gerente dice "El cliente activó la suscripción", en el código debe haber un método activateSubscription(), no setStatus(1). TypeScript con su tipado estricto es ideal para esto: los nombres de clases, métodos y variables se convierten en documentación.
Bounded Context (Contexto delimitado)
\nUn sistema grande se divide en contextos independientes: "Pagos", "Gestión de usuarios", "Envíos". Cada contexto tiene su propio modelo y lenguaje. Los módulos de TypeScript y los tipos estrictos ayudan a separar rígidamente estos contextos a nivel de importaciones.
\n\nConstruyendo el modelo de dominio en TypeScript
\n\nValue Object (Objeto de valor)
\nEs una entidad inmutable que se define por sus atributos, no por un identificador. Ejemplo: Email, Money, Coordinates.
\n\n// value-objects/Email.ts\nexport class Email {\n private readonly value: string;\n\n constructor(email: string) {\n if (!this.isValid(email)) {\n throw new Error('Invalid email address');\n }\n this.value = email;\n }\n\n private isValid(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n }\n\n public getValue(): string {\n return this.value;\n }\n\n public equals(other: Email): boolean {\n return this.value === other.value;\n }\n}\n\n\nObserva: Email valida su propia validez. Ningún servicio debe saber cómo se ve un email correcto. Esto es Domain-Driven Design en TypeScript puro: la regla de negocio vive dentro del objeto.
Entity (Entidad)
\nA diferencia de Value Object, una entidad tiene un identificador (ID) y puede cambiar con el tiempo. Ejemplo: User, Order.
\n\n// entities/User.ts\nimport { Email } from '../value-objects/Email';\nimport { UserId } from '../value-objects/UserId';\n\nexport class User {\n private readonly id: UserId;\n private email: Email;\n private isActive: boolean;\n\n constructor(id: UserId, email: Email) {\n this.id = id;\n this.email = email;\n this.isActive = true;\n }\n\n public getId(): UserId {\n return this.id;\n }\n\n public changeEmail(newEmail: Email): void {\n if (!this.isActive) {\n throw new Error('Cannot change email for inactive user');\n }\n this.email = newEmail;\n }\n\n public deactivate(): void {\n this.isActive = false;\n }\n}\n\n\nAggregate (Agregado)
\nUn agregado es un grupo de entidades y objetos de valor que se manejan como una unidad. El agregado tiene una raíz (Aggregate Root) a través de la cual se accede a los objetos internos.
\n\nEjemplo: un pedido (Order)