← dev-notes
DDD · PARTE 2 DE 4

Diseño Táctico
Los bloques de construcción

Entities, Value Objects, Aggregates, Invariantes y la distinción entre Domain Services y Application Services

DDD · P2
🧱 Entities vs. Value Objects 🌳 Aggregates e Invariantes ⚙️ Domain vs. App Services ✅ Reglas de diseño
01 Entity vs. Value Object — la distinción más importante
Comparación crítica — Entity vs. Value Object
ENTITY
Identidad que persiste en el tiempo
Tiene un ID único que la define. Dos entidades con el mismo ID son el mismo objeto aunque todos sus atributos cambien.
Es mutable. Su estado cambia a lo largo del ciclo de vida (ej: un Pedido pasa de PENDING → PAID → SHIPPED).
Se compara por identidad: order1.id == order2.id
Se persiste con su ID como clave primaria.
🪪 Analogía: Una persona. Si Juan cambia de nombre, dirección y trabajo, sigue siendo Juan — porque su RUT/ID no cambió.
VS
VALUE OBJECT
Describe una característica, sin identidad
No tiene ID. Dos VOs son iguales si todos sus valores son iguales: Money(100,"CLP") == Money(100,"CLP")
Es inmutable. Para "modificar" un VO, creas uno nuevo. Nunca mutas el existente.
Se compara por valor: igualdad estructural de todos sus atributos.
Puede persistirse embebido dentro de la Entity que lo contiene (columna de la misma tabla).
💵 Analogía: Un billete de $1000. No importa cuál billete en particular tienes — lo que importa es el valor. Dos billetes de $1000 son intercambiables.
Entity — con identidad e IDJAVA
public class Order {
    private final OrderId id;    // identidad
    private OrderStatus status;   // mutable
    private Money total;

    // igualdad por identidad, no por atributos
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Order other)) return false;
        return id.equals(other.id);
    }
}
Value Object — inmutable e igualdad por valorJAVA
// record = VO perfecto: inmutable + equals por valor
public record Money(BigDecimal amount,
                      String currency) {

    public Money {
        // validación en el constructor
        if (amount.compareTo(BigDecimal.ZERO) < 0)
            throw new InvalidMoneyException(...);
    }

    public Money add(Money other) {
        return new Money(amount.add(other.amount), currency);
    }
}
CUÁNDO USAR CADA UNO
Usa Entity si…
Necesitas rastrear el objeto en el tiempo. Tiene ciclo de vida. Cambios de estado importan. Se persiste como fila con ID.
Usa Value Object si…
Es una medida, cantidad, rango o descripción. No importa "cuál" instancia específica tienes. Dos instancias con los mismos valores son intercambiables.
VOs comunes
Money · Email · PhoneNumber · Address · DateRange · Percentage · Coordinates
02 Aggregates — fronteras de consistencia e Invariantes
🎯
La palabra mágica en entrevistas senior: "Invariante"
Cuando te pregunten sobre Aggregates, no digas solo "grupo de entidades". Di: "El Aggregate Root es el guardián de las invariantes del negocio. Ninguna operación puede dejar al Aggregate en un estado que viole sus reglas de negocio." Eso es lo que diferencia una respuesta senior de una junior.
Aggregate — Order como Aggregate Root
AGGREGATE ROOT
Order
id · status · customerId
ENTITY
OrderItem
productId · qty
VALUE OBJECT
Money
amount · currency
VALUE OBJECT
Discount
percentage · code
INVARIANTES QUE EL AGGREGATE PROTEGE
Un pedido confirmado no puede tener 0 líneas de producto.
El total siempre debe ser igual a la suma de los items menos los descuentos.
No se pueden añadir items a un pedido en estado CANCELLED o SHIPPED.
El descuento nunca puede superar el 100% del total.
📐
REGLAS DEL AGGREGATE
4 reglas de oro para diseñar Aggregates
REGLA 1 — Una sola Aggregate Root por Aggregate
Solo se accede a los objetos internos (OrderItem) a través de la raíz (Order). Nunca se obtiene un OrderItem directamente del repositorio.
REGLA 2 — Boundaries pequeños
Un Aggregate grande es un problema de contención (lock). Preferir Aggregates pequeños con 1-3 entidades y referencias a otros Aggregates solo por ID.
REGLA 3 — Consistencia eventual entre Aggregates
Dentro de un Aggregate: consistencia inmediata (transacción). Entre Aggregates: consistencia eventual vía Domain Events. Nunca en la misma transacción.
REGLA 4 — Referencia por ID fuera del boundary
Order no contiene un objeto Customer completo. Contiene un CustomerId. Evita acoplar Aggregates distintos.
Order.java — comportamiento rico, no anémicoJAVA
public class Order {
    // La lógica vive en el dominio, no en el servicio
    public void addItem(OrderItem item) {
        if (status != DRAFT)
            throw new OrderNotEditableException();
        items.add(item);
        this.total = recalculate();  // invariante
    }

    public void confirm() {
        if (items.isEmpty())
            throw new EmptyOrderException();
        this.status = CONFIRMED;
        registerEvent(new OrderConfirmed(this.id));
    }
}
03 Domain Services vs. Application Services — dónde va cada lógica
Distinción clave — Domain Service vs. Application Service
DOMAIN SERVICE
Lógica de negocio sin hogar
Contiene lógica de negocio que no pertenece naturalmente a ninguna entidad porque involucra múltiples Aggregates o conceptos del dominio.
Vive en la capa de dominio. No tiene estado. Es stateless.
Sus métodos tienen nombres del Ubiquitous Language: transferFunds(), calculateShippingCost()
Puede recibir Ports como parámetros (si necesita algo del exterior), pero no los inyecta en el constructor.
🧮 Ejemplo: TransferService.transfer(accountFrom, accountTo, amount) — la lógica de transferencia no pertenece solo a Account origen ni a Account destino.
VS
APPLICATION SERVICE
El director de orquesta
Contiene lógica de orquestación, no de negocio. Coordina repositorios, domain services y eventos pero no toma decisiones de negocio.
Vive en la capa de aplicación (Use Case). Implementa los Input Ports de la arquitectura hexagonal.
Sigue el patrón: obtener → ejecutar → persistir → publicar eventos. Siempre en ese orden.
Gestiona la transacción y la seguridad (autorización). El dominio no sabe nada de transacciones.
🎼 Analogía: El director de orquesta sabe en qué orden entran los músicos (repositorios, servicios, eventos), pero no toca ningún instrumento (lógica de negocio).
Domain Service — lógica que involucra múltiples AggregatesJAVA
// Vive en domain/ — lógica pura sin frameworks
public class TransferDomainService {

    public void transfer(
        Account from, Account to, Money amount) {

        // regla de negocio: ¿puede transferir?
        if (!from.hasSufficientFunds(amount))
            throw new InsufficientFundsException();

        from.debit(amount);   // método rico en la entidad
        to.credit(amount);   // método rico en la entidad
    }
}

// Nótese: sin @Service, sin @Transactional, sin repos
Application Service — orquestador sin lógica de negocioJAVA
@UseCase
public class TransferMoneyUseCase {

    private final AccountRepository accounts;
    private final TransferDomainService transferService;
    private final EventPublisher events;

    public void execute(TransferCommand cmd) {
        // 1. Obtener
        Account from = accounts.findById(cmd.fromId());
        Account to   = accounts.findById(cmd.toId());
        // 2. Ejecutar (lógica en el Domain Service)
        transferService.transfer(from, to, cmd.amount());
        // 3. Persistir
        accounts.save(from); accounts.save(to);
        // 4. Publicar eventos del dominio
        events.publish(from.domainEvents());
    }
}
🧠
La pregunta que te harán en la entrevista
"¿Dónde pones la lógica de descuento de un pedido?" — La respuesta correcta depende del contexto. Si la regla es solo sobre Order ("si tiene más de 5 items, aplica 10%"): va en la Entity (order.applyDiscount()). Si involucra múltiples Aggregates ("si el cliente es VIP y compró más de $X este mes en todos sus pedidos"): va en un Domain Service. Si es orquestación pura (obtener cliente, calcular, guardar): Application Service.