DDD táctico: entidades, value objects y agregados | Nicolás Garzón
DDD táctico proporciona patrones para modelar comportamiento e invariantes dentro de un bounded context. No debe aplicarse como vocabulario decorativo ni convertir cada tabla en una entidad o agregado.
Después de definir un límite semántico, necesitas representar identidad, valores, reglas y consistencia.
Texto
Copiar entidades
+ value objects
+ agregados
+ servicios de dominio
+ eventos
+ repositoriosCada patrón responde a un problema distinto.
Tiene identidad continua aunque cambien sus atributos.
Un pedido sigue siendo el mismo durante su ciclo de vida:
Texto
Copiar draft → confirmed → preparing → deliveredLa igualdad depende de identidad, no de que todos los campos coincidan.
TypeScript
Copiar class Order {
private constructor (
readonly id: OrderId,
private status: OrderStatus,
private readonly lines: OrderLine[ ] ,
) { }
confirm ( ) : void {
if ( this . status !== "draft" ) {
throw new InvalidOrderTransition ( this . status, "confirmed" ) ;
}
if ( this . lines. length === 0 ) {
throw new EmptyOrderCannotBeConfirmed ( ) ;
}
this . status = "confirmed" ;
}
} La entidad protege transición y evita que consumidores cambien status libremente.
Se define por su valor, suele ser inmutable y valida un concepto.
TypeScript
Copiar class Money {
private constructor (
readonly minorUnits: number ,
readonly currency: string ,
) { }
static create ( minorUnits: number , currency: string ) : Money {
if ( ! Number. isInteger ( minorUnits) ) throw new Error ( "Invalid amount" ) ;
if ( minorUnits < 0 ) throw new Error ( "Negative amount" ) ;
if ( ! currency. trim ( ) ) throw new Error ( "Currency required" ) ;
return new Money ( minorUnits, currency. toUpperCase ( ) ) ;
}
add ( other: Money) : Money {
if ( other. currency !== this . currency) {
throw new Error ( "Currency mismatch" ) ;
}
return Money. create ( this . minorUnits + other. minorUnits, this . currency) ;
}
}
Estado válido desde construcción.
Operaciones con lenguaje propio.
Igualdad por valor.
Menos primitive obsession.
Un value object no necesita ser una clase si el lenguaje ofrece otra forma segura. Importa la semántica.
Es una frontera de consistencia. Agrupa lo mínimo que debe mantenerse válido dentro de una transacción.
Texto
Copiar Order aggregate
├── Order root
└── OrderLinesSolo la raíz recibe modificaciones externas. Otros agregados se referencian por ID.
Un agregado no es un árbol completo de relaciones ni una representación exacta del documento de UI.
¿Qué invariantes requieren consistencia inmediata?
¿Qué objetos cambian juntos?
¿Qué necesita bloquearse o versionarse como unidad?
¿Qué puede converger posteriormente?
Texto
Copiar Order
→ total y líneas deben ser consistentes
Inventory
→ disponibilidad y reservas deben ser consistentesNo metas inventario dentro de pedido solo porque una venta lo consulta. Son responsabilidades y ciclos de vida distintos.
Agregados grandes aumentan conflictos.
TypeScript
Copiar await repository. save ( order, expectedVersion) ; Optimistic concurrency puede verificar una versión y rechazar lost updates. La aplicación decide si reintenta, recarga o muestra conflicto.
La consistencia también puede apoyarse en constraints y operaciones atómicas. El modelo no reemplaza garantías de persistencia.
Representan una operación de dominio que no pertenece naturalmente a una entidad o value object.
TypeScript
Copiar class PricingPolicy {
calculate ( lines: OrderLine[ ] , customerTier: CustomerTier) : Money {
}
} No deben convertirse en contenedores genéricos. Si toda lógica vive en services y las entidades solo tienen datos, el modelo es anémico.
Expresan un hecho relevante dentro del contexto:
TypeScript
Copiar type OrderConfirmed = {
orderId: string ;
occurredAt: string ;
} ; Pueden activar comportamiento interno. Antes de publicarlos externamente, suele ser necesario traducirlos a eventos de integración con contrato estable.
Ofrecen acceso conceptual a raíces de agregado:
TypeScript
Copiar interface OrderRepository {
findById ( id: OrderId) : Promise < Order | null > ;
save ( order: Order, expectedVersion: number ) : Promise < void > ;
} No deberían exponer cualquier filtro o tabla. Las queries de lectura pueden usar otros modelos.
Cuando construir un agregado requiere varias reglas o dependencias, una factory puede expresar el proceso sin dejar objetos incompletos.
Texto
Copiar InventoryItem aggregate
├── productId
├── available
├── reservations
└── version
Se recibe ReserveStock con tenant, producto, cantidad y request ID.
El repository carga el agregado correcto.
El agregado rechaza cantidad inválida o insuficiente.
Detecta una reserva repetida mediante request ID.
Reduce disponibilidad y crea una reserva.
Se guarda con versión esperada.
Se registra StockReserved.
Cantidad cero o negativa.
Reserva duplicada.
Dos solicitudes concurrentes.
Expiración de reserva.
Cancelación después de consumo.
Producto inexistente.
El agregado protege reglas, pero la expiración puede ser coordinada por un job o proceso de aplicación.
Una invariante que cruza servicios no puede protegerse simplemente creando un agregado conceptual enorme. Debes:
Rediseñar el límite.
Usar reservas.
Aceptar consistencia eventual.
Coordinar una saga.
Definir compensaciones.
Los agregados ayudan a mantener transacciones locales.
Un agregado puede mapearse a varias tablas. No necesita reflejar la estructura ORM.
Valida estados imposibles.
Preserva versión.
Evita disparar eventos de creación nuevamente.
Maneja datos históricos compatibles.
Reglas e invariantes relevantes.
Ciclos de vida complejos.
Lenguaje de negocio importante.
Concurrencia y estados válidos.
CRUD simple.
Datos sin comportamiento.
Prototipo desechable.
Problema todavía no comprendido.
En esos casos, modelos simples pueden ser más claros.
Entidad por tabla.
Agregado gigante.
Setters públicos.
Servicios con toda la lógica.
Repositories genéricos.
Eventos usados como notificaciones técnicas.
Modelar relaciones en lugar de invariantes.
Ignorar concurrencia.
Value objects: construcción e igualdad.
Entidades: transiciones e invariantes.
Agregados: secuencias y concurrencia lógica.
Repositories: integración y versionado.
Eventos: hechos correctos y no duplicados.
Entidad se define por identidad.
Value object se define por valor y protege conceptos.
Agregado es frontera transaccional, no árbol completo.
La raíz protege invariantes.
Servicios de dominio se usan cuando una operación no pertenece a un objeto.
Persistencia y dominio deben colaborar sin confundirse.
¿Por qué Money no debería ser solo un número?
¿Qué criterio define un agregado?
¿Por qué un agregado grande afecta concurrencia?
¿Cuándo usarías un servicio de dominio?
¿Qué diferencia existe entre evento de dominio e integración?
Integración entre bounded contexts explica cómo estos modelos colaboran sin compartir internals ni eliminar autonomía semántica.