Domain Model (DDD-Bausteine)

Zielgruppe: Architekt:in und Entwickler:in. Erklärt die taktischen DDD-Bausteine, die in common-domain bereitgestellt werden, und wie die Aggregate je Bounded Context aussehen.

Begriffe (kurz, ausführlicher im Glossar)

Begriff Kurzerklärung
Bounded Context Abgegrenzter fachlicher Geltungsbereich eines Modells; derselbe Begriff ("Copy"/Exemplar) kann in zwei Contexts unterschiedliche Bedeutung haben.
Aggregate Konsistenz-/Transaktionsgrenze; eine Gruppe von Objekten, die immer zusammen in einem gültigen Zustand sein müssen.
Aggregate Root Der einzige Einstiegspunkt in ein Aggregate von außen (siehe AggregateRoot).
Value Object Unveränderliches, wertbasiertes Objekt ohne eigene Identität (Isbn, Money, EmailAddress).
Entity Objekt mit fachlicher Identität über die Zeit (z. B. eine konkrete Loan), im Gegensatz zum Value Object.
Domain Event "Etwas fachlich Bedeutsames ist im Aggregate passiert" - intern, bevor es zum Integrationsevent wird (siehe ADR-0003).
Rich Domain Model vs. Anemic Domain Model Reich: Invarianten leben IM Aggregate (Loan.renew() prüft selbst, ob eine Verlängerung erlaubt ist). Anämisch: Entities sind nur Datencontainer, alle Regeln stehen in Services davor - hier bewusst vermieden (siehe InvariantViolationException).

Framework-freier Kern (common-domain)

Ein zentrale Entscheidung dieser Codebasis: common-domain hat keine Abhängigkeit zu Spring, Jakarta oder Hibernate (siehe common-domain/pom.xml und die ArchUnit-Regeln in architecture-tests/.../CommonDomainPurityTest.java, die das bei jedem Build erzwingen).

Warum das wichtig ist: Domainlogik, die sich nicht ohne einen laufenden Spring-Kontext testen lässt, ist ein Warnsignal ("ich kann Loan.renew() nicht testen, ohne @SpringBootTest zu verwenden") - meist ein Zeichen, dass sich fachliche Regeln mit Infrastruktur-Code vermischt haben. Ein reiner new Loan(...) -Aufruf in einem JUnit-Test ohne jeden Container ist das Ziel.

Die Kehrseite: jeder Service muss sein Domainmodell separat auf JPA-Entities abbilden (kein @Entity direkt auf der Domainklasse). Diesen Trade-off ("Persistence Ignorance" vs. weniger Mapping-Code) behandelt 02-entwicklung/persistenz-mapping.md in den Service-Implementierungsphasen genauer.

Aggregate je Bounded Context

catalog-service: Book (mit Copy-Exemplaren)

member-service: Member

lending-service: Loan

reservation-service: Reservation

fine-service: Fine

notification-service

Context Map

flowchart LR
    subgraph Core["Core Domain"]
        lending[lending-service]
    end
    subgraph Supporting["Supporting Subdomains"]
        catalog[catalog-service]
        member[member-service]
        reservation[reservation-service]
        fine[fine-service]
    end
    subgraph Generic["Generic Subdomain"]
        notification[notification-service]
    end

    lending -->|Customer/Supplier: REST, synchron| catalog
    lending -->|Customer/Supplier: REST, synchron| member
    lending -->|Published Language: Kafka Events| reservation
    lending -->|Published Language: Kafka Events| fine
    reservation -->|Published Language: Kafka Events| catalog
    fine -->|Published Language: Kafka Events| notification
    lending -->|Published Language: Kafka Events| notification

Core Domain (lending-service): hier liegt der eigentliche Wettbewerbsvorteil/die Kernkomplexität einer Bibliotheksverwaltung (Ausleihregeln, Sagas) - bekäme in einem echten Projekt die meiste Entwicklerzeit. Supporting Subdomains unterstützen den Kernprozess, sind aber für sich genommen nicht das Alleinstellungsmerkmal. Generic Subdomain (notification-service) ließe sich in der Praxis auch durch ein Standardprodukt (z. B. einen SaaS-E-Mail-Dienst) ersetzen - diese fachliche Einordnung (Core/Supporting/Generic, nach Eric Evans) hilft bei der Priorisierung, wo sich Investition in Domain-Modellierung lohnt.

⌂ Cockpit