Cómo elegir aggregate roots

Utilizar un modelo de dominio rico, basado en las ideas propuestas por DDD, se ha convertido en algo habitual. Independientemente de que uses un proceso DDD real, con su experto de dominio, su lenguaje ubicuo y demás, este tipo de modelos tienen su utilidad y, aunque no son aplicables en todos los casos, hay situaciones en las que encajan muy bien.

OJO: Hay muchas aplicaciones en las que no tiene sentido tener un modelo de dominio complejo, e incluso generalmente dentro de una misma aplicación hay muchas partes que no lo necesitan. En este post asumo que ya has decidido (para bien o para mal) usarlo y que sabes por qué lo quieres usar.

Los elementos básicos para construir un modelo de dominio (entidades, value objects, repositorios, etc.) son bastante fáciles de entender, pero hay algo que parece que resulta más complicado y por lo que he recibido bastantes consultas: cómo decidir qué entidades son aggregate roots.

Qué son los agregados

Para poder encontrar los aggregate roots (¿raíces de agregados?) de un modelo, lo primero que tenemos que tener claro es qué es un agregado. Un agregado es un grupo de clases que mantienen un invariante de forma conjunta. La raíz de ese agregado es el objeto «padre» que nos permite interactuar con el grupo de clases.

El ejemplo canónico de esto sería una factura con sus líneas, donde la raíz del agregado sería la factura y las líneas, la información de los productos, los datos del cliente o la dirección de envío serían entidades o value objects (dependiendo del diseño) que forman parte del agregado.

Si trabajamos con agregados, deberíamos seguir ciertas regla (como siempre, tómate estas reglas más como recomendaciones que como leyes innegociables):

  • Todas las interacciones con clases que forman parte de un agregado deberíamos hacerlas a través del aggregate root. Esto es necesario para que podamos garantizar los invariantes.
  • Sólo podemos recuperar de la base de datos (a través de repositorios o como más te guste) aggregate roots. Nunca recuperaremos entidades internas al agregado.
  • Si necesitamos mantener una relaciones entre entidades de diferentes agregados, siempre será hacia el aggregate root, nunca hacia clases internas del agregado.

Existe otra faceta que debemos considerar a la hora de decidir cuáles son los agregados quue forman parte de nuestra aplicación, y es la transaccionalidad. Para mi esto es una consecuencia del primer requisito (el mantenimiento de invariantes), pero hay quien le da más importancia a la parte transaccional y se basa más en ella a la hora de detectar los agregados.

La idea es que, puesto que necesitamos garantizar que los invariantes se mantienen dentro del agregado, toda modificación que realicemos sobre él deberá realizarse en una misma transacción.

Aunque esto es cierto y en la vida real es un factor fundamental, creo que nos puede hacer pensar más en el plano meramente tecnológico (gestión de transacciones en la base de datos) que en el plano conceptual, y eso nos puede generar confusión a la hora de diseñar nuestro modelo. En cualquier caso, la transaccionalidad de las operaciones sobre un agregado es importante y debes tenerla en cuenta también.

Mucha gente cuando intenta pasar de un enfoque data centric a un enfoque model centric acaba en una situación extrema.

A veces se diseña un modelo ultraconectado, en el que todo forma un único agregado que cuelga de la entidad Company o similar. Ese modelo parece muy cómodo porque todas la relaciones son navegables y no hay que andar tirando de acceso a base de datos para recorrerlas, pero implica un acomplamiento muy alto y problemas de eficiencia porque tendemos a cargar demasiada información cada vez que queremos hacer un proceso.

En otras ocasiones se va al extremo opuesto y no se crea ningún agregado. Se tienen un montón de entidades independientes, cada una con su propio repositorio (o equivalente) para cargarlas de la base de datos, y el mantenimiento de invariantes se hace a través de servicios de aplicación, haciendo que, en realidad, estemos trabajando con el mismo modelo anémico de siempre, pero cambiando los DataSets por algo que hemos llamado entidades y nos hace sentirnos más modernos.

Cómo elegirlos

¿Cómo podemos agrupar entidades pero no caer en el extremo del modelo ultraconectado?

La clave es pensar en los invariantes. Vamos a volver al modelo (aburrido pero conocido por todos) de la factura y sus líneas. Simplificando mucho podemos tener un modelo inicial parecido a éste:

modelo muy conectado

Aquí tenemos un modelo totalmente conectado en el que la factura mantiene referencias al usuario que la ha creado, el cliente al que se le ha emitido y sus líneas, las cuales a su vez mantienen referencias al producto que se ha vendido y, éste, al impuesto que lleva asociado.

A simple vista, siguiendo la dirección de las dependencias, podríamos pensar que tenemos un único agregado cuya raíz es la clase Invoice, pero si nos centramos en invariantes la cosa cambia. Por ejemplo, ¿podemos cambiar la dirección de email del usuario que la ha emitida sin que afecte a la factura? Probablemente sí. Sin embargo, ¿podemos modificar las líneas de una factura una vez que ésta ha sido cerrada? Claramente no.

Si lo vemos desde el punto de vista transaccional/operacional, pasa algo parecido. Tendremos operaciones para añadir líneas a una factura mientras la estamos construyendo, y tendremos operaciones para modificar usuario, pero sería extraño tener una operación que a la vez modifique la factura y el usuario que la creó.

Eso nos va a ir dando pistas de qué forma parte realmente del agregado y qué no. La factura y sus líneas deben mantener invariantes que afectan a ambas, pero la factura y el usuario no, por lo que segurametne formen parte de diferentes agregados, uno que contenga la factura y sus líneas y otro que contenga el usuario.

¿Qué pasa con el cliente o con los productos? Aquí entra en escena otro factor que a veces pasamos por alto, y es que algunos conceptos que son entidades en determinados contextos, en otros son sólamente value objects.

A lo largo del tiempo un cliente puede cambiar de nombre fiscal, o de dirección fiscal, o incluso de CIF, pero una vez que una factura ha sido emitida, esos datos no pueden cambiar. Lo mismo para con un producto, puede que en el futuro cambie su descripción o el impuesto que se le aplica, pero aunque el gobierno decida subir el IVA una vez más, la factura que emitimos hace 3 años no debería verse modificada por ello.

Esto nos lleva a un modelo más parecido a éste:

modelo menos conectado

Aquí vemos que, en nuestro cutre modelo de ejemplo (perdón por la calidad de la imagen, pero lo del dibujo no es lo mío), acabaríamos con 4 agregados. El agregado de Invoice mantendría una referencia a otro agregado, el de User, que está formado por una única entidad (la propia clase User), pero ha perdido las referencias al resto de agregados a base de desnormalizar parte de la información.

Por supuesto, todo este modelo es discutible y, sin tener más contexto, es imposible saber si es una buena o mala decisión, pero espero que como ejemplo nos sirva para hacernos una idea de cómo podríamos dividir nuestro modelo en distintos agregados.

Por si alguno se pregunta qué pasa con Address, que forma parte de dos agregados, es importante aclarar que no, no es que forme parte de dos agregados. Address sería un value object que representa una dirección y que podemos usar en distintas entidades. De hecho, y precisamente por ser un value object (y por tanto inmutable), podríamos incluso compartir la misma instancia, aunque esto no es lo habitual.

Si pensamos en cómo se relaciona este modelo con la gestión de transacciones, veremos que encaja naturalmente con lo que esperamos. Si operamos sobre una factura, cargaremos una instancia de la clase Invoice, ejecutaremos métodos sobre ella, que nos garantizará que sus invariantes se mantienen, y podremos guardarla de forma transaccional en la base de datos.

La pregunta más frecuente en este sentido suele ser, ¿qué pasa cuando una transacción afecta a más de un agregado? El hecho de que un agregado deba ser modificado de forma transaccional no implica que en cada transacción sólo se pueda modificar un agregado (ese un error de concepto frecuente que lleva al extremo que mencionaba antes de tener un único agregado del que «cuelga» toda la aplicación). Podemos tener una transacción que cargue varios agregados de la base de datos, opere sobre ellos y los guarde de forma transaccional.

Lo que sí debemos tener en cuenta es que cuantos más agregados deba coordinar nuestra transacción, más probable es que podamos acabar con problemas de rendimiento, bloqueos y (esperemos que no) interbloqueos. En esos casos, sustituir parte de la transaccionalidad por un sistema de consitencia eventual basado en mensajes asíncronos puede ser una buena solución, pero el incremento de complejidad es considerable y, si te lo puedes ahorrar, mejor.

Al separar los distintos agregados de esta forma conseguimos además hacernos una idea de lo que podrían acabar siendo diferentes bounded contexts. En nuestro ejemplo, podríamos acabar planteando un bounded context para el catálogo de productos que, segurammente, sería factible implementar con un enfoque puramente centrado en los datos, y otro bounded context para la facturación donde sí podemos sacarle más partido a un modelo más complejo y rico.

Conclusiones

El diseño de modelos tiene su arte. Es algo que requiere experiencia, práctica y, sobre todo, conocimiento del negocio que se está modelando.

Existen muchas formas de diseñar modelos y todas tienen sus casos de uso, pero si has decidido que para una parte de tu aplicación es importante tener un modelo de dominio, merece la pena dedicar tiempo a aprender cómo construirlo y no limitarse a aplicar el mismo enfoque de siempre, seguir pensando en tablas de la base de datos, y limitarte a llamar a las cosas entidades, como si el cambio de nombres ya fuese suficiente.

Cuando vayas a utilizar un modelo de dominio y necesites definir qué entidades forman parte de cada agregado, piensa en las operaciones que necesitas sobre el modelo y en los invariantes que deben mantenerse en cada operación. Esa es la mejor guía para detectar qué entidades forman parten de un agregado y poder construir un modelo más sólido.

7 comentarios en “Cómo elegir aggregate roots

  1. Juan Araujo dijo:

    Buen dia , tengo una duda sobre la eleccion de agregados , se puede elegir como agregado a una entidad que no tenga hijos ?

  2. Juan Araujo dijo:

    Siguiendo el ejemplo , seria recomendable que todas las entidades unicas que tengan persistencia sean marcadas como agregadas ? y una pregunta mas , las clases hijas dentro del contexto de un agregado las marcarias como entidades ? o son simples clases ? , gracias por la ayuda.

  3. Toda entidad forma parte de un agregado, pero no toda entidad es raíz de un agregado (que es un grupo de entidades que mantiene un invariante de forma conjunta).

    Independientemente de eso, las clases que «cuelgan» de una entidad, ya sea raíz de agregado o no, pueden ser entidades, value objects o «clases normales».

  4. Excelente artículo, muy aclaratorio.

    Hay otra situación que no se nombra. Es en la que una entidad es raíz en un contexto y agregado en otro. Entiendo que no existe ningún problema por esto.

  5. Gracias Alberto.

    El caso que mencionas es interesante. En principio, no habría problema. Es el caso de User en el ejemplo, que es raíz de su agregado (formado sólo por él mismo), y es a la vez referenciado por otro agregado.

    Aun así, en muchos casos puede ser útil separar por completo ambos usos para separar más rígidamente distintos bounded contexts. Así, podrías tener la entidad User como agregado en el bounded context de Identification, y un value object, UserInfo, que se usara para identificar usuarios en otros agregados sin acoplarlos al bounded context de identificación.

  6. Matías Peronetto dijo:

    Hola Juanma, tengo la siguiente duda, sobre todo para persistir los agregados:

    Mi inquietud es sobre si es recomendable crear un modelo distinto ajeno al dominio en la capa de infraestructura de datos, que sea el que contenga toda la dependencia de EF, liberando de esa responsabilidad al dominio.
    Esto supone el uso de mappers para convertir de objetos de dominio de datos a objetos de dominio y viceversa, para no contaminar el dominio con problemas de la persistencia.

    ¿Como se corresponderían dichos objetos de la capa de dominio/infra de datos? Es decir ¿Por cada uno de mis objetos de dominio, deberia crear un objeto de dominio de datos (DTO)?.

    En un ejemplo típico donde tenemos un agredado compuesto de una Entity ‘Cliente’ y un Value-object ‘Direccion’, donde ‘Cliente’ es mi root. Mi agregado tendria un único repositorio, y éste repo deberia leer/persistir mis objetos del dominio usando las clases DTO mapeadas del dominio de datos.

    Saludos!

Comentarios cerrados.