Domain-Driven Design: Factories, creación de objetos e invariantes desde el inicio
Aprende cuándo usar un constructor, un método estático o una Factory dedicada para crear objetos de dominio completos. Construye un pedido desde una cotización en TypeScript, comprueba sus invariantes y distingue una creación nueva de la reconstitución de un estado persistido.
Una tienda convierte una cotización en pedido. El código crea primero un Order vacío, después asigna el cliente, copia líneas, calcula precios y establece el estado. Otro proceso copia casi la misma secuencia, pero olvida comprobar que la cotización siga vigente.
Durante la construcción existen pedidos sin cliente o sin líneas. Si alguien los publica en una colección compartida, los guarda antes de tiempo o deja una excepción a mitad del recorrido, el resto del sistema puede recibir un objeto incompleto.
Una Factory permite dar nombre y límites a esa creación: recibe lo necesario y entrega un objeto completo que cumple las reglas iniciales, o rechaza la operación. No necesita guardar, cobrar ni responder HTTP para cumplir ese cometido.
Qué responsabilidad asume una Factory
En DDD, una Factory encapsula la construcción de objetos o Aggregates cuando esa tarea expone complejidad que el consumidor no debería gestionar. El resultado debe estar completo y satisfacer sus Invariants, las condiciones que definen sus estados válidos. Evans desarrolla esta responsabilidad en la sección Factories de su referencia de DDD.
La Factory protege el comienzo; el objeto creado debe proteger su evolución. Si una Factory construye correctamente un pedido, pero después cualquier consumidor puede borrar sus líneas confirmadas, el modelo sigue teniendo un problema.
Tampoco debe existir una única clase universal que construya todos los objetos. Una operación como fromAcceptedQuote() expresa una intención concreta; createObject(type, data) obliga a interpretar códigos y combinaciones de datos que pueden esconder reglas diferentes.
La Factory de DDD no exige implementar una jerarquía de clases ni reproducir obligatoriamente los patrones GoF llamados Factory Method o Abstract Factory. Aquí nos interesa la responsabilidad de construir el modelo; la forma elegida debe responder a la complejidad real.
Cuándo un constructor es suficiente
Si construir un valor solo requiere comprobar una condición local, un constructor puede ser claro y suficiente. Por ejemplo, para representar una cantidad de plazas positiva:
// Programa autocontenido: seats.ts.import assert from'node:assert/strict';
classSeats {
readonlycount: number;
constructor(count: number) {
if (!Number.isSafeInteger(count) || count <= 0) {
thrownewError('Las plazas deben ser un entero positivo');
}
this.count = count;
Object.freeze(this);
}
}
assert.equal(newSeats(20).count, 20);
assert.throws(() =>newSeats(0), /entero positivo/);
Una SeatsFactory.createSeats() que solo llame a ese constructor no aporta necesariamente una decisión nueva. Tener parámetros tampoco convierte automáticamente la creación en compleja.
Busca otra forma cuando el consumidor necesita coordinar componentes, elegir entre variantes, establecer reglas iniciales o recordar una secuencia que no debería repetirse por toda la aplicación.
Un método estático puede expresar la intención
Un constructor solo ofrece el nombre de la clase. Un método estático permite nombrar la forma de creación. Fowler muestra esa posibilidad en Replace Constructor with Factory Function.
En reservas, BookingWindow.between(start, end) puede ser más expresivo que un constructor ambiguo con números. Este programa es independiente del resto:
Usamos instantes enteros en milisegundos y no negativos para esta demostración. No estamos resolviendo horarios locales, calendarios ni disponibilidad de una sala. La ventana válida es una condición para reservar, no una reserva concedida.
Este ejemplo enseña que puedes mejorar la creación sin introducir una clase Factory independiente. El nombre ayuda a expresar el acuerdo; no reemplaza las validaciones.
Ejemplo principal: crear un pedido desde una cotización
De una secuencia frágil a una operación completa
La alternativa problemática suele parecerse a este fragmento parcial, con una API de setters ficticia:
const order = newMutableOrder();
order.setCustomerId(quote.customerId);
order.setSourceQuoteId(quote.id);
for (const line of quote.lines) {
order.addLine(line.productId, line.quantity, line.unitPriceMinorUnits);
}
order.setStatus('draft');
El consumidor conoce el orden de ensamblaje y puede olvidar pasos. La mejora será factory.fromAcceptedQuote(orderId, quote): toda la construcción ocurre antes de entregar el pedido.
Reglas iniciales y simplificaciones
La tienda permite convertir una cotización aprobada mientras siga dentro de su plazo de conversión. Establecemos que:
La cotización tiene estado accepted y el instante actual es anterior a su vencimiento.
El pedido nace en draft, con cliente, referencia de cotización y al menos una línea.
Cada línea tiene identificador local, producto, cantidad positiva y precio válido.
No se repiten productos ni identificadores de línea.
Los cálculos monetarios deben permanecer dentro del rango de enteros seguros.
Todos los precios están en céntimos de EUR. No hay descuentos, impuestos, cobros, stock ni garantías de unicidad global. El Use Case proporciona el identificador del pedido y obtiene una cotización confiable; la Factory no consulta proveedores para reunir esos datos.
El reloj será una dependencia explícita. Así podremos comprobar el límite exacto de vencimiento sin esperar ni alterar el reloj del sistema.
Programa completo y pruebas
Guarda el siguiente bloque como order-factory.test.ts. Se ejecuta con node --test order-factory.test.ts usando Node.js 24.21.0, sin paquetes externos.
Los programas anteriores se ejecutan con node seats.ts y node booking-window.ts en la misma versión. La ejecución directa utiliza type stripping, que elimina anotaciones de TypeScript, pero no realiza comprobación estática de tipos.
La entrada tiene dos libros de 12 EUR y un cuaderno de 3 EUR, con cotización aprobada hasta el instante 2000. El reloj devuelve 1000. La Factory comprueba esa precondición, asigna identificadores locales a las líneas y solicita un pedido nuevo en draft con un total implícito de 27 EUR.
Los programas seats.ts y booking-window.ts completaron sus aserciones, y las cuatro pruebas de order-factory.test.ts pasaron con Node.js 24.21.0. No se realizó análisis estático con TypeScript ni pruebas de persistencia o integraciones externas.
El Aggregate valida su estructura: referencias no vacías, líneas válidas, productos distintos y cálculos representables. La Factory dedicada no recibe permiso para eludir esas condiciones. start() es también un método de creación y solo admite el estado inicial acordado.
Reutilizamos internamente restore() para construir los componentes y comprobar el estado común. Eso no convierte creación y reconstitución en la misma operación de negocio: start() fija draft; restore() conserva el estado recibido. No existe lógica de vencimiento ni efectos de creación dentro de restore().
Si una línea es inválida, el consumidor no recibe un pedido incompleto. Se pueden haber creado objetos locales que luego se descartan, pero no se ha guardado ni publicado nada. Esa propiedad de la construcción en memoria no equivale a una transacción distribuida ni revierte efectos externos que alguien añada después.
Las Invariants del objeto y las precondiciones de creación
Conviene distinguir dos tipos de regla. «Todo pedido tiene líneas válidas» debe cumplirse también al reconstruirlo. «Esta cotización sigue disponible para convertirse ahora» solo se evalúa al iniciar ese proceso.
Una Factory que comprueba todo y devuelve un objeto con setters sin restricciones deja las Invariants desprotegidas. En el extremo contrario, obligar a reconstruir un pedido histórico con las condiciones comerciales de hoy puede impedir cargar datos perfectamente legítimos.
El ejemplo acepta snapshots tipados y conocidos, no JSON arbitrario. Un adaptador real validaría estructura y versión antes de reconstruir. Si las reglas estructurales cambian, hay que decidir cómo interpretar o migrar estados históricos; reconstituir no significa confiar ciegamente en cualquier dato guardado.
El constructor private restringe llamadas durante la comprobación de TypeScript, pero no es una frontera de seguridad durante la ejecución. La documentación de clases de TypeScript distingue esas restricciones de los campos privados #. Conserva validaciones y controla además las entradas públicas de tu código.
Educación: un objeto del Domain puede crear otro
Una convocatoria puede ofrecer createEnrollment() si conoce sus condiciones de apertura y puede iniciar una inscripción con la referencia correcta a esa convocatoria. Este fragmento muestra solo el método de una clase, no un programa independiente:
Aquí asumimos que requireOpenForEnrollment() comprueba la apertura y Enrollment.start() establece sus condiciones iniciales. El objeto creador aporta conocimiento que ya posee; no debería comenzar a consultar APIs o cargar historiales ajenos solo para conservar el método dentro de la clase.
Crear la inscripción tampoco garantiza una plaza, evita duplicados globales ni la guarda. Si la capacidad es una restricción, el proceso que la asigna necesita su propia garantía. Este caso enseña a reconocer cuándo un método de creación pertenece naturalmente a un objeto existente y qué promesas no debe hacer.
Lo que una Factory no debería absorber
Una clase que consulta una cotización por HTTP, cobra una tarjeta, crea el pedido, lo guarda y envía un correo está coordinando mucho más que construcción. Separar esas tareas permite saber qué condiciones se comprobaron y qué fallos pueden dejar efectos pendientes.
Un Use Case puede obtener los datos, invocar la Factory y coordinar el guardado. El acceso técnico queda en sus adaptadores. Una Factory puede colaborar con una política del Domain cuando la creación lo requiere; no tiene por qué convertirse en un servicio general de aplicación.
Si los datos proceden de otro Bounded Context, mantén separadas sus implementaciones. Una integración por ACL HTTP con contrato del consumidor puede entregar la información necesaria. La Factory no convierte esa consulta y el guardado posterior en una transacción compartida.
Tampoco garantiza que una cotización se convierta una sola vez en pedido. Dos llamadas pueden crear dos objetos válidos con identificadores diferentes. Si el negocio exige una única conversión, el sistema debe protegerla al registrar la operación mediante mecanismos adecuados de unicidad o concurrencia.
Cuándo elegir cada forma
Usa un constructor cuando la creación sea breve, sus parámetros tengan un significado claro y pueda garantizar por sí solo el estado válido.
Prefiere un método estático cuando ayude a distinguir intenciones, fijar valores iniciales o nombrar alternativas de creación. No lo llames automáticamente Factory Method en el sentido del patrón GoF: un método estático con nombre no implica una jerarquía polimórfica.
Considera una Factory dedicada cuando el ensamblaje de varios componentes, las variantes o las colaboraciones de creación oculten la intención del objeto principal. Su responsabilidad debería poder describirse sin incluir guardar, notificar y responder al usuario.
Y utiliza un objeto del Domain como creador cuando ya disponga del conocimiento adecuado y esa operación sea una parte natural de su responsabilidad. Ninguna forma exige abstraer otras diez posibilidades que todavía no existen.
Prompts para orientar y revisar Factories con IA
Evaluar si existe complejidad de creación real
Analiza estas secuencias de construcción. Para cada una, propón constructor, método estático, creación desde un objeto existente o Factory dedicada. Justifica qué complejidad oculta y qué reglas iniciales garantiza. No modifiques archivos ni añadas jerarquías por defecto. Una persona validará las intenciones del negocio y si las diferencias son reales o solo nombres distintos para lo mismo.
Implementar una construcción completa
Implementa OrderFactory.fromAcceptedQuote en TypeScript sin dependencias. Recibe el identificador del pedido, cotización aprobada y reloj explícito. Rechaza el vencimiento exacto, crea líneas válidas y devuelve un pedido draft completo. No consultes HTTP, cobres, guardes ni notifiques. Mantén también las validaciones estructurales en el Aggregate. Entrega código y pruebas de entradas inválidas y no compartir estado mutable. Una persona ejecutará las pruebas y validará la política de conversión.
Revisar creación y reconstitución
Revisa estos caminos de creación y restore. Busca regeneración de identidad, pérdida de estado confirmado, aplicación de vencimientos actuales a datos históricos y efectos externos dentro de constructores. No cambies archivos. Devuelve evidencia y escenarios observables; distingue preguntas de errores. Una persona comprobará el formato persistido y las reglas históricas antes de decidir migraciones o excepciones.
La IA puede generar una Factory que solo envuelve new o, al contrario, una clase que concentra todo el proceso. Revisa la responsabilidad concreta y los caminos que podrían entregar objetos incompletos, no solo el nombre del archivo.
Tres ejercicios con solución razonada
1. Una cotización vencida y un pedido histórico
Un pedido se creó ayer desde una cotización válida entonces. Hoy la Factory rechaza convertirla porque venció. ¿Debe fallar también la carga del pedido existente?
Solución razonada: no por ese motivo. La reconstitución conserva la identidad, fecha y estado ya registrados; no intenta iniciar otra conversión. Debe validar las condiciones de integridad aplicables al estado guardado y tratar versiones históricas de forma explícita, sin volver a cobrar ni consultar la vigencia actual de la cotización.
2. Veinte Factories que solo llaman a constructores
La IA crea una Factory para cada Value Object, todas sin decisiones ni dependencias adicionales. ¿Qué revisarías?
Solución razonada: qué responsabilidad aportan y qué coste introducen. Si solo duplican una llamada clara, un constructor o método estático puede bastar. Mantén una abstracción cuando oculte una decisión real o establezca un contrato útil, no para completar una lista de patrones.
3. Un correo enviado antes de terminar
La Factory envía el correo «pedido creado» y luego descubre una línea duplicada. ¿Basta con lanzar una excepción?
Solución razonada: no; la excepción no retira el correo. Construye y valida antes de iniciar efectos externos, y coordina notificaciones con el resultado durable del proceso según sus necesidades. Una Factory de construcción no debería prometer que guardar y notificar siempre ocurren juntos.
Cómo reconocer una Factory útil
Busca una operación de creación con intención clara, entradas explícitas y un resultado completo. Comprueba que los errores no expongan objetos parcialmente configurados y que las reglas no dependan de que todos los consumidores recuerden una secuencia de setters.
Después revisa los límites: crear no es guardar, validar una precondición no es reservar un recurso y reconstituir no es iniciar otra vida. Una Factory bien situada oculta la complejidad necesaria sin absorber responsabilidades ajenas ni sustituir la protección del objeto que construye.
Diseña un OrderRepository independiente del almacenamiento y compáralo con un DAO, un ORM y un Application Service. Implementa adaptadores en memoria y SQLite, recupera Aggregates completos y comprueba guardado, conflictos de versión y rollback con pruebas ejecutables.
Diseña un Order Aggregate con una raíz que controla sus líneas y protege reglas comunes. Con TypeScript y pruebas ejecutables, aprende a elegir límites de consistencia, evitar cambios parciales y distinguir las garantías del modelo de las que requiere la persistencia transaccional.
Aprende a definir Modules a partir de responsabilidades del negocio, ofrecer interfaces públicas y controlar sus dependencias. Con un ejemplo ejecutable en TypeScript, distingue un módulo de una carpeta, una layer o un Bounded Context y establece límites útiles para trabajar con IA.