Domain-Driven Design: Cómo Usar Agregados Lean

Un agregado lean en Domain-Driven Design es una entidad raíz que se mantiene enfocada en una única responsabilidad de negocio, minimizando la cantidad de datos que carga en memoria con cada operación de escritura. En vez de meter todo (tareas, miembros, documentos) en un único objeto gigante, dividís la lógica en agregados más pequeños que se comunican mediante eventos de dominio, evitando así table locks, contención de escrituras concurrentes y cuellos de botella de performance.

En 30 segundos

  • Los agregados monolíticos que guardan todo (tareas, miembros, documentos) en una sola clase causan table locks y contención cada vez que escribís en la base de datos.
  • Cargar un agregado completo para validar invariantes es caro: si el Project tiene 500 Tasks, cada operación dispara un query masivo.
  • Los agregados lean separan responsabilidades: un agregado Task independiente, otro Project independiente, comunicados vía Domain Events.
  • Refactorizar hacia agregados más pequeños significa identificar límites claros, distribuir las reglas de negocio, y confiar en eventos para mantener la integridad.
  • Domain Events (TaskAssigned, ProjectCompleted) son el mecanismo para que agregados lean se comuniquen sin crear dependencias directas.

Qué es un agregado en Domain-Driven Design

Un agregado en DDD es un cluster de objetos de dominio que se trata como una unidad para fines de persistencia y cambio. Tiene una raíz — el agregado root — que es el punto de entrada a través del cual otros objetos acceden al resto de la jerarquía. (Spoiler: la mayoría del tiempo, esa raíz se llena de más cosas de lo que debería.)

La idea original es buena: la raíz protege los invariantes del dominio. Si estás construyendo un sistema de gestión de proyectos y Project es tu agregado root, entonces Project es el responsable de asegurar que no haya estados inválidos — tareas no asignadas a miembros existentes, proyectos completados con tareas abiertas, documentos adjuntos a proyectos ya cerrados.

Eso sí: proteger invariantes no debería significar cargar todo lo que existe en el universo cada vez que querés cambiar algo.

El antipatrón: agregados monolíticos

Acá es donde la mayoría nos metemos en quilombo. Diseñamos Project así:

“`
class Project {
– id: UUID
– name: String
– tasks: List<Task>
– teamMembers: List<TeamMember>
– documents: List<Document>
– budget: Budget
– status: ProjectStatus

+ assignTask(taskId, memberId): void
+ addMember(member): void
+ attachDocument(doc): void
+ completeProject(): void
+ adjustBudget(amount): void
}
“`

Y encapsulamos todas las reglas en una sola clase: no podés asignar una tarea a alguien que no es miembro, no completás un proyecto si quedan tareas abiertas, no adjuntas documentos a un proyecto terminado. Lógicamente está correcto, subís el modelo, lo probás en local, funciona bárbaro, lo mandás a producción y de repente todo se rompe porque cada operación de escritura dispara un query que carga el proyecto completo con todos sus datos asociados, se bloquean las filas de la tabla, las escrituras se encolan, y los timeouts llueven. Tema relacionado: ejecutar agentes sin dependencias externas.

¿Por qué? Porque en DDD, antes de ejecutar cualquier cambio, cargás el agregado completo. Necesitás el objeto entero en memoria para validar que no violás ninguna regla. Si Project tiene 200 tareas, 50 miembros y 300 documentos, y vos querés asignar una tarea a un miembro, igual cargás los 200 + 50 + 300 registros para que Project.assignTask() verifique que el miembro existe y que el proyecto está activo.

Problemas de performance: table locks y contención

Los table locks son el síntoma. La causa es que múltiples escrituras concurrentes golpean la misma tabla esperando cargar el mismo agregado.

Imaginá este escenario real:

  • Dos usuarios intenten asignar tareas diferentes al mismo proyecto. ✓ Operaciones independientes.
  • Pero ambas disparan un SELECT en la tabla Project que bloquea las filas, esperando el lock. ✗ Se crea contención.
  • La segunda escritura espera a que la primera termine. ✗ Si la base de datos está ocupada, ambas mueren por timeout.

Esto escala mal. Con 100 usuarios concurrentes en el mismo proyecto, tenés 100 requests bloqueados esperando el lock. Un agregado monolítico en un sistema con alta concurrencia es una bomba de relojería.

El paper de Denis Kyashif sobre agregados lean (2026) lo explica claramente: los problemas empiezan a la superficie apenas el sistema crece, pero la raíz del problema está en el diseño inicial.

Principios de agregados lean

Un agregado lean sigue un principio simple: cada agregado debe tener una única razón para cambiar. No intentás resolver todo dentro de una sola clase.

En lugar de eso:

  • Project es un agregado que contiene solo: id, name, status, owner. Nada más.
  • Task es su propio agregado con: id, title, status, assignee, projectId (referencia).
  • TeamMember es otro agregado: id, name, email, skills, projectId (referencia).
  • Document es también independiente: id, name, url, projectId (referencia).

Cada agregado es pequeño, enfocado, rápido de cargar. Las invariantes se distribuyen: Project valida que existe; Task valida que el asignado es miembro (pero usa la referencia, no carga el objeto completo). Si la validación requiere datos de otros agregados, usás Domain Events.

Cómo refactorizar hacia agregados más pequeños

El proceso tiene tres pasos.

Paso 1: Identificar límites de responsabilidad.

Mirá tu agregado monolítico y preguntate: ¿Cuáles de estos datos cambian juntos? ¿Cuáles tienen ciclos de vida independientes? Las tareas pueden asignarse, completarse, eliminarse sin que el Project cambie. Los miembros pueden joinear o irse del proyecto sin que las tareas existentes se vean afectadas. Son límites claros. Ya lo cubrimos antes en consideraciones de privacidad en arquitecturas.

Paso 2: Crear agregados separados con referencias.

En lugar de guardar objetos completos, guardás IDs:

  • Task.projectId: UUID (no el objeto Project completo)
  • Task.assigneeId: UUID (no el objeto TeamMember completo)

Cuando cargás una Task, no cargás el Project ni el miembro. Si necesitás datos de ellos, hacés queries separadas (o usás un data loader para evitar N+1).

Paso 3: Validación distribuida.

Las reglas se distribuyen entre agregados. Si asignás una tarea a alguien que no es miembro, lo que pasa es que Task.assign() dispara un evento “TaskAssignmentAttempted”. Un handler escucha ese evento, verifica que el miembro existe, y emite “TaskAssigned” o “TaskAssignmentFailed”. O simplemente dejás que la aplicación maneje el error cuando intenta referenciar un miembro inexistente (depende del nivel de integridad que necesites).

Validación de invariantes en agregados lean

Acá viene la pregunta incómoda: si los datos están distribuidos, ¿cómo garantizás que las reglas de negocio se cumplen?

Respuesta: confía en eventos de dominio.

Ejemplo: la regla es “no podés adjuntar documentos a un proyecto completado”.

  • Document.attach(projectId) se ejecuta.
  • Document emite un evento: “DocumentAttachmentRequested”.
  • Un handler escucha ese evento, consulta el estado del Project (un query simple), y si está completado, cancela la operación.
  • Si está activo, Project emite “DocumentAttached” (confirmando la acción).

Eso sí: esto introduce eventual consistency (no strong consistency). La validación no es instantánea. Pero en la mayoría de las aplicaciones, eventual consistency es perfectamente aceptable y te evita los table locks.

Patrones de comunicación entre agregados

Domain Events son el mecanismo estándar. Cada agregado puede emitir eventos cuando cambia:

  • TaskAssigned: cuando asignás una tarea.
  • ProjectCompleted: cuando terminas un proyecto.
  • TeamMemberRemoved: cuando un miembro se va.

Otros agregados (o servicios de aplicación) escuchan esos eventos y reaccionan. Task emite TaskAssigned; Project escucha y actualiza su conteo de tareas asignadas. No hay dependencia directa entre ellos. Complementá con herramientas modernas para desarrolladores.

¿El lado negativo? Necesitás un event bus (puede ser en memoria, RabbitMQ, Kafka, lo que sea). Y los errores en handlers no revierten la operación original — necesitás compensación.

Agregados monolíticos vs. lean: comparación

CaracterísticaMonolíticoLean
Tamaño en memoriaGrande (carga todo)Pequeño (solo esencial)
Contención de escrituraAlta (mismo lock)Baja (locks independientes)
Complejidad de validaciónCentralizada (una clase)Distribuida (eventos)
ConsistencyStrong (inmediata)Eventual (con latencia)
Escalabilidad concurrentePobreBuena
CouplingAlto (todo unido)Bajo (referencias)
domain-driven design agregados lean diagrama explicativo

Ejemplos prácticos

Ejemplo 1: Plataforma de gestión de tareas con miles de usuarios.

Si cada proyecto tuviera el monolítico, un proyecto con 10.000 tareas significaría cargar esos 10.000 registros cada vez que asignás una tarea. Con agregados lean, cargas solo la Task (un registro). Performance: 100x más rápido. Contención: prácticamente cero.

Ejemplo 2: Sistema de documentos colaborativo.

Document y Project son independientes. Cuando publicas un documento, emitís un evento “DocumentPublished”. Project escucha y actualiza su lista de documentos públicos, pero solo en background — la operación de publicación no espera. El usuario ve el resultado al instante, sin la latencia de actualizar Project.

Errores comunes en la refactorización

Error 1: Cargar agregados relacionados de todas formas.

Intentás refactorizar, dividís en agregados, pero luego hacés esto: cuando cargás Task, automáticamente ejecutás un query para cargar Project y TeamMember también. Eso anula toda la ventaja. Si necesitás esos datos, consultalos explícitamente (y ponelos en caché si hace falta), no como parte de la carga automática.

Error 2: Olvidar qué agregado es responsable de qué regla.

Dividís los datos pero dejas las validaciones flotando entre servicios. “No podés completar un proyecto si hay tareas abiertas” — ¿dónde vive esa regla? ¿En Project? ¿En Task? ¿En un servicio externo? Si no está claro, vas a tener bugs. Cada invariante debe vivir explícitamente en un agregado. Lo explicamos a fondo en plataformas para gestionar tu código.

Error 3: No usar eventos de dominio; en su lugar, hacer consultas sincrónicas.

Asignás una tarea y luego hacés un SELECT en TeamMember para validar. Todavía estás acoplado. Domain Events permiten desacoplamiento real: Task emite TaskAssigned, punto. Si alguien necesita validar, que escuche el evento.

Preguntas Frecuentes

¿Qué son los agregados lean en Domain-Driven Design?

Agregados lean son entidades raíz enfocadas en una única responsabilidad, que cargan solo los datos esenciales en memoria. En lugar de un agregado gigante que contiene tareas, miembros y documentos, cada uno es un agregado independiente que se comunica mediante Domain Events. Resultado: mejor performance y menor contención de escritura.

¿Cómo evito que un agregado se llene de datos innecesarios?

Seguí el principio de responsabilidad única. Preguntate: ¿estos datos cambian juntos? ¿tienen el mismo ciclo de vida? Si no, son agregados separados. Guardá referencias (IDs) en lugar de objetos completos. Un Task tiene projectId, no el objeto Project.

¿Por qué los agregados grandes causan problemas de performance?

Porque cada operación de escritura carga el agregado completo en memoria y bloquea sus filas en la base de datos. Si tienes 500 tareas en el agregado y quiero asignar una, cargo las 500. Con múltiples usuarios, se crean locks que esperan, contención, y timeouts. Agregados pequeños = menos datos cargados = menos contención.

¿Cómo valido reglas de negocio si los datos están distribuidos entre agregados?

Con Domain Events y eventual consistency. Task emite un evento cuando sucede algo; otros agregados escuchan y reaccionan. Ejemplo: Task.assign() emite “TaskAssigned”; Project escucha y sabe que debe actualizar su conteo. No es validación inmediata, pero funciona y escala.

¿Necesito un event bus para implementar agregados lean?

Técnicamente no (podés hacerlo en memoria), pero en un sistema real sí. Necesitás algo que distribuya eventos entre agregados: RabbitMQ, Kafka, Redis Pub/Sub, o un simple event store. Sin eso, los agregados quedan acoplados de nuevo y perdés las ventajas.

Conclusión

Domain-Driven Design con agregados lean es el antídoto contra los cuellos de botella de performance que surgen cuando metés todo en una sola clase. No es magia — es disciplina de diseño. Identificás responsabilidades, separás datos, y usás Domain Events para mantener la integridad sin crear acoplamiento.

¿Significa que los agregados monolíticos son un error? No siempre. En sistemas pequeños o con baja concurrencia, pueden estar bien. Pero en cualquier cosa que crezca — más usuarios, más datos, más operaciones concurrentes — los agregados lean son el camino. Y lo bueno es que podés empezar con monolítico y refactorizar hacia lean cuando sientas que la performance se degrada. El diseño de Domain-Driven Design te permite hacerlo sin reescribir todo desde cero.

Fuentes

Similar Posts