Un cop traçades les fronteres estratègiques amb els Bounded Contexts, necessitem construir el model de domini dins de cada context. El disseny tàctic del DDD ens proporciona un conjunt de patrons d'implementació —Value Objects, Entitats, Agregats, Repositoris, Serveis de Domini i Esdeveniments de Domini— que ens permeten plasmar les regles del negoci en codi robust i expressiu. Aquesta lliçó és la més pràctica del mòdul: treballarem amb exemples en Java que veuràs reflectits en projectes reals. Dominar aquests patrons és el que diferencia un model anèmic (dades sense regles) d'un model ric que protegeix la integritat del negoci.

Contingut

  1. Value Objects (objectes de valor)
  2. Entitats
  3. Agregats i arrel d'agregat
  4. Repositoris
  5. Serveis de domini
  6. Esdeveniments de domini

  1. Value Objects (objectes de valor)

Un Value Object és un objecte que es defineix pels seus atributs, no per una identitat. Dos Value Objects amb els mateixos valors són intercanviables. Són immutables: un cop creats, no canvien.

Exemples típics: una quantitat de diners, una data, una adreça, un rang de dates.

public final class Dinero {
    private final BigDecimal importe;
    private final String moneda;

    public Dinero(BigDecimal importe, String moneda) {
        if (importe == null || moneda == null) {
            throw new IllegalArgumentException("Importe i moneda són obligatoris");
        }
        this.importe = importe;
        this.moneda = moneda;
    }

    public Dinero sumar(Dinero otro) {
        if (!this.moneda.equals(otro.moneda)) {
            throw new IllegalArgumentException("No es poden sumar monedes diferents");
        }
        return new Dinero(this.importe.add(otro.importe), this.moneda);  // retorna NOU objecte
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Dinero)) return false;
        Dinero d = (Dinero) o;
        return importe.compareTo(d.importe) == 0 && moneda.equals(d.moneda);
    }

    @Override
    public int hashCode() { return Objects.hash(importe, moneda); }
}

Punts clau d'aquest Dinero:

  • La classe és final i tots els seus camps són final: és immutable, no hi ha setters.
  • El mètode sumar no modifica l'objecte actual; retorna un nou Dinero. Aquesta és l'essència de la immutabilitat.
  • S'inclou una regla de negoci (no sumar monedes diferents) dins del mateix objecte.
  • Se sobreescriuen equals i hashCode basant-se en els valors: dos Dinero de 10 EUR són iguals encara que siguin instàncies diferents. Això és el que defineix un Value Object.

Avantatge pràctic: usar Dinero en lloc d'un BigDecimal solt evita errors com sumar euros amb dòlars o passar l'import sense la moneda.

  1. Entitats

Una Entitat és un objecte que té una identitat única i contínua en el temps, independent dels seus atributs. Encara que canviïn totes les seves dades, continua sent la mateixa entitat. Una Poliza, un Asegurado o un Siniestro són entitats: identificades pel seu número/ID, no pels seus atributs.

public class Siniestro {
    private final IdSiniestro id;     // identitat: no canvia mai
    private EstadoSiniestro estado;   // els atributs poden canviar
    private Dinero importeReclamado;

    public Siniestro(IdSiniestro id, Dinero importeReclamado) {
        this.id = Objects.requireNonNull(id);
        this.importeReclamado = Objects.requireNonNull(importeReclamado);
        this.estado = EstadoSiniestro.ABIERTO;
    }

    public void cerrar() {
        if (estado == EstadoSiniestro.CERRADO) {
            throw new IllegalStateException("El sinistre ja està tancat");
        }
        this.estado = EstadoSiniestro.CERRADO;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Siniestro)) return false;
        return this.id.equals(((Siniestro) o).id);   // igualtat per IDENTITAT
    }

    @Override
    public int hashCode() { return id.hashCode(); }
}

Diferències amb el Value Object:

  • La identitat id és final i defineix l'entitat. Encara que canviïn estado i importeReclamado, continua sent el mateix sinistre.
  • equals i hashCode es basen només en l'id, no en tots els atributs. Dos sinistres amb el mateix id són el mateix, encara que difereixin en estat.
  • L'entitat pot mutar (cerrar() canvia l'estat), però sempre a través de mètodes que protegeixen les regles.
Aspecte Value Object Entitat
Identitat No en té; es defineix pels seus valors Té un ID propi i estable
Mutabilitat Immutable Mutable (de manera controlada)
Igualtat Per valors (equals sobre atributs) Per identitat (equals sobre l'ID)
Exemples Diners, Data, Adreça Pòlissa, Sinistre, Assegurat

  1. Agregats i arrel d'agregat

Un Agregat és un grup d'entitats i Value Objects que es tracten com una unitat de consistència. Té una entitat principal anomenada arrel de l'agregat (Aggregate Root), que és l'única porta d'entrada a l'agregat: tot accés des de fora hi passa.

Regles fonamentals dels agregats:

  • Només es referencia l'arrel des de fora. Els objectes interns no s'exposen directament.
  • L'arrel protegeix les invariants (regles que sempre s'han de complir) de tot l'agregat.
  • Un agregat es desa i es carrega complet, com una unitat transaccional.
  • Entre agregats, les referències es fan per identitat (ID), no per objecte.
// Poliza és l'ARREL de l'agregat; Cobertura és una entitat interna
public class Poliza {
    private final IdPoliza id;
    private final List<Cobertura> coberturas = new ArrayList<>();
    private Dinero primaTotal;

    // L'accés a les cobertures passa SEMPRE per l'arrel
    public void anadirCobertura(TipoCobertura tipo, Dinero capital) {
        if (coberturas.size() >= 10) {
            throw new IllegalStateException("Màxim 10 cobertures per pòlissa");
        }
        Cobertura c = new Cobertura(tipo, capital);
        coberturas.add(c);
        recalcularPrima();          // l'arrel manté la INVARIANT de coherència
    }

    private void recalcularPrima() {
        Dinero total = new Dinero(BigDecimal.ZERO, "EUR");
        for (Cobertura c : coberturas) {
            total = total.sumar(c.calcularPrima());
        }
        this.primaTotal = total;
    }

    // S'exposa una còpia de només lectura, mai la llista interna mutable
    public List<Cobertura> getCoberturas() {
        return Collections.unmodifiableList(coberturas);
    }
}

Anàlisi detallada:

  • Poliza és l'arrel. Cobertura és una entitat interna que no es manipula directament des de fora.
  • Per afegir una cobertura, s'usa anadirCobertura() a l'arrel, que aplica la invariant "màxim 10 cobertures" i recalcula la prima. Si deixéssim modificar la llista per fora, aquesta regla es podria trencar.
  • recalcularPrima() garanteix que la prima total sempre sigui coherent amb les cobertures: aquesta és una invariant de l'agregat.
  • getCoberturas() retorna una llista inmodificable: l'exterior pot llegir, però no alterar l'estat intern saltant-se les regles.
graph TD
    subgraph Agregado_Poliza[Agregat: Poliza]
        R[Poliza - ARREL] --> C1[Cobertura 1]
        R --> C2[Cobertura 2]
        R --> PT[primaTotal - Value Object]
    end
    EXT[Codi extern] -->|nomes accedeix a l'arrel| R

Aquest diagrama mostra que el codi extern només "veu" l'arrel Poliza; les cobertures i la prima són internes i protegides.

Regla d'or: una transacció modifica un sol agregat. Si necessites canviar-ne diversos, normalment és senyal que els has de coordinar amb esdeveniments de domini (apartat 6), no en la mateixa transacció.

  1. Repositoris

Un Repositori proporciona la il·lusió d'una col·lecció en memòria d'arrels d'agregat, ocultant els detalls de persistència (base de dades, etc.). Hi ha un repositori per agregat (per la seva arrel), no per cada entitat interna.

// La INTERFÍCIE viu a la capa de domini: parla de Poliza, no de SQL
public interface RepositorioPolizas {
    Optional<Poliza> buscarPorId(IdPoliza id);
    void guardar(Poliza poliza);          // desa l'agregat COMPLET
    List<Poliza> buscarVigentesDe(IdAsegurado asegurado);
}

Comentaris sobre la interfície:

  • Viu al domini i usa exclusivament conceptes del domini (Poliza, IdPoliza). No menciona JDBC, JPA ni SQL: el domini no ha de saber com es persisteix.
  • guardar(Poliza) persisteix l'agregat complet (la pòlissa amb les seves cobertures), respectant que l'agregat és la unitat de consistència.
  • Només exposa operacions amb sentit al domini (buscar pòlisses vigents d'un assegurat).
// La IMPLEMENTACIÓ viu a la capa d'infraestructura
@Repository
public class RepositorioPolizasJpa implements RepositorioPolizas {
    private final EntityManager em;

    public RepositorioPolizasJpa(EntityManager em) { this.em = em; }

    @Override
    public Optional<Poliza> buscarPorId(IdPoliza id) {
        return Optional.ofNullable(em.find(Poliza.class, id.valor()));
    }

    @Override
    public void guardar(Poliza poliza) { em.merge(poliza); }

    @Override
    public List<Poliza> buscarVigentesDe(IdAsegurado asegurado) {
        return em.createQuery(
                "SELECT p FROM Poliza p WHERE p.asegurado = :a AND p.vigente = true",
                Poliza.class)
            .setParameter("a", asegurado.valor())
            .getResultList();
    }
}

El més important d'aquesta implementació:

  • Implementa la interfície del domini però conté els detalls tècnics (JPA, EntityManager, JPQL). Aquesta separació és el que permet canviar la base de dades sense tocar el domini.
  • És una aplicació directa del principi d'inversió de dependències: el domini defineix la interfície, la infraestructura la implementa.

  1. Serveis de domini

De vegades una operació del negoci no pertany de manera natural a cap entitat ni Value Object. Quan una lògica important del domini implica diversos agregats o conceptes, es modela com un Servei de Domini: un objecte sense estat que exposa una operació amb nom del negoci.

public class ServicioTarificacion {

    private final TablaActuarial tabla;

    public ServicioTarificacion(TablaActuarial tabla) { this.tabla = tabla; }

    // Operació del domini que no encaixa en una sola entitat
    public Dinero calcularPrima(PerfilRiesgo perfil, TipoCobertura cobertura) {
        BigDecimal factor = tabla.factorPara(perfil.edad(), perfil.profesion());
        BigDecimal base = cobertura.capitalAsegurado().importe();
        return new Dinero(base.multiply(factor), "EUR");
    }
}

Per què això és un Servei de Domini i no un mètode d'una entitat:

  • Calcular la prima combina un PerfilRiesgo, una TablaActuarial i un TipoCobertura. No "pertany" a cap d'ells en exclusiva.
  • El servei no té estat propi: només orquestra el càlcul amb les dades que rep.
  • El seu nom, calcularPrima, és un verb del Llenguatge Ubic. No és un "Helper" ni un "Utils" genèric.

Compte: no abusis dels serveis de domini. Si tota la lògica acaba en serveis i les entitats queden buides, has tornat al model anèmic. Usa serveis només quan l'operació realment no càpiga en una entitat.

  1. Esdeveniments de domini

Un Esdeveniment de Domini representa quelcom rellevant que ha ocorregut al domini i que pot interessar a altres. Es nomena en passat: PolizaSuscrita, SiniestroCerrado. Permeten que diferents agregats (fins i tot de diferents Bounded Contexts) reaccionin sense acoblar-se directament.

// L'esdeveniment és immutable i descriu un fet consumat del passat
public final class PolizaSuscrita {
    private final IdPoliza idPoliza;
    private final IdAsegurado idAsegurado;
    private final Instant fecha;

    public PolizaSuscrita(IdPoliza idPoliza, IdAsegurado idAsegurado) {
        this.idPoliza = idPoliza;
        this.idAsegurado = idAsegurado;
        this.fecha = Instant.now();
    }
    // getters de només lectura...
}
// L'arrel publica l'esdeveniment quan ocorre el fet
public class Poliza {
    private final List<Object> eventos = new ArrayList<>();

    public void suscribir(IdAsegurado asegurado) {
        // ... lògica de subscripció i validació d'invariants ...
        this.eventos.add(new PolizaSuscrita(this.id, asegurado));  // registra l'esdeveniment
    }

    public List<Object> eventosPendientes() {
        return Collections.unmodifiableList(eventos);
    }
}

Com funciona i per què importa:

  • Quan s'executa suscribir(), la pòlissa registra un esdeveniment PolizaSuscrita en lloc de cridar directament altres mòduls.
  • Després de desar l'agregat, la infraestructura publica aquests esdeveniments. Altres contextos (per exemple, Facturació o Notificacions) es poden subscriure i reaccionar (emetre el primer rebut, enviar la benvinguda).
  • Això desacobla els agregats: la Poliza no sap ni li importa qui reacciona a la seva subscripció. És la base de les arquitectures dirigides per esdeveniments.

Representat com a flux:

sequenceDiagram
    participant U as Usuari
    participant P as Poliza (Subscripcio)
    participant B as Facturacio
    participant N as Notificacions
    U->>P: suscribir()
    P->>P: registra PolizaSuscrita
    P-->>B: PolizaSuscrita
    P-->>N: PolizaSuscrita
    B->>B: emet primer rebut
    N->>N: envia email de benvinguda

El diagrama de seqüència mostra com un únic esdeveniment PolizaSuscrita desencadena reaccions independents a Facturació i Notificacions, sense que Subscripció conegui aquests detalls.

Errors Comuns i Consells

  • Agregats massa grans. Ficar mitja base de dades dins d'un sol agregat genera bloquejos i problemes de rendiment. Mantén els agregats petits; referencia altres agregats per ID.
  • Modificar diversos agregats en una transacció. Trenca la regla de consistència. Usa esdeveniments de domini per coordinar canvis entre agregats.
  • Repositoris per entitat interna. Només les arrels d'agregat tenen repositori. No creïs un RepositorioCoberturas si Cobertura viu dins de l'agregat Poliza.
  • Value Objects mutables. Si afegeixes setters a un Value Object, en perds les garanties. Mantén-los immutables i retorna còpies noves a les operacions.
  • Abusar dels serveis de domini. Buiden les entitats i reapareix el model anèmic. Pregunta't sempre si la lògica càpiga en una entitat abans de crear un servei.
  • Consell: comença pels Value Objects. Substituir primitius (BigDecimal, String, int) per conceptes del domini (Dinero, Email, IdPoliza) ja millora enormement l'expressivitat i seguretat del model.

Exercicis

Exercici 1. Decideix, per a cada concepte del domini d'una biblioteca, si el modelaries com a Entitat o com a Value Object, justificant-ho: (a) un exemplar físic d'un llibre; (b) un ISBN; (c) un usuari de la biblioteca; (d) un període de préstec (data d'inici i fi).

Exercici 2. Donat un agregat Pedido que conté LineaPedido, escriu la signatura d'un mètode a l'arrel que afegeixi una línia respectant la invariant "el total de la comanda no pot superar 5.000 EUR". Explica per què el mètode va a l'arrel i no a LineaPedido.

Exercici 3. Identifica quin patró tàctic (Value Object, Entitat, Servei de Domini, Esdeveniment de Domini o Repositori) usaries per a cada cas: (a) desar i recuperar comandes; (b) representar un percentatge de descompte; (c) transferir saldo entre dos comptes; (d) notificar que "una comanda ha estat enviada".

Solucions

Solució 1. (a) Entitat: cada exemplar físic té identitat pròpia (se'n pot perdre o deteriorar un de concret). (b) Value Object: un ISBN es defineix pel seu valor; dos ISBN iguals són intercanviables. (c) Entitat: l'usuari té identitat estable encara que canviïn les seves dades. (d) Value Object: un període és immutable i es defineix per les seves dates.

Solució 2. Una signatura vàlida: public void anadirLinea(Producto producto, int cantidad). El mètode va a l'arrel Pedido perquè la invariant "el total no supera 5.000 EUR" depèn de totes les línies en conjunt. Una sola LineaPedido no coneix el total de la comanda, així que no pot garantir aquesta regla; només l'arrel, que veu el conjunt complet, ho pot fer.

Solució 3. (a) Repositori (de l'arrel Pedido). (b) Value Object (un percentatge immutable, definit pel seu valor). (c) Servei de Domini (la transferència implica dos agregats Cuenta i no pertany a un de sol). (d) Esdeveniment de Domini (PedidoEnviado, un fet consumat del passat).

Conclusió

En aquesta lliçó hem recorregut l'arsenal tàctic del DDD: els Value Objects immutables definits pels seus valors, les Entitats amb identitat estable, els Agregats que agrupen objectes sota una arrel que protegeix les invariants, els Repositoris que oculten la persistència, els Serveis de Domini per a la lògica que no encaixa en una entitat, i els Esdeveniments de Domini que desacoblen reaccions entre parts del sistema. Amb aquests patrons pots construir un model ric i fidel al negoci dins de cada Bounded Context.

Fins ara hem vist els contextos per separat. A l'última lliçó del mòdul, "Mapatge de Contextos (Context Mapping)", estudiarem com es relacionen i s'integren els diferents Bounded Contexts entre si mitjançant patrons com ACL, Shared Kernel o Open Host Service.

Curs d'Arquitectura d'Aplicacions

Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions

Mòdul 2: Principis i Tàctiques de Disseny

Mòdul 3: Estils i Patrons Arquitectònics

Mòdul 4: Arquitectures Distribuïdes i Microserveis

Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria

Mòdul 6: Disseny Dirigit pel Domini (DDD)

Mòdul 7: Dades i Persistència

Mòdul 8: Arquitectura al Núvol i Desplegament

Mòdul 9: Qualitat, Seguretat i Observabilitat

Mòdul 10: Evolució, Governança i Casos Pràctics

© Copyright 2026. Tots els drets reservats