Domain Model (DDD-Bausteine)
Zielgruppe: Architekt:in und Entwickler:in. Erklärt die taktischen DDD-Bausteine, die in
common-domainbereitgestellt 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)
- Invarianten: eine ISBN ist eindeutig; ein
Copykann nicht gleichzeitigVERFÜGBARundAUSGELIEHENsein; einBookohne mindestens einCopykann nicht ausgeliehen werden. - Value Objects:
Isbn(mit Prüfziffernvalidierung).
member-service: Member
- Invarianten: ein Mitglied mit offenen Mahngebühren über einer Tarif-abhängigen Obergrenze darf nicht weiter ausleihen (wird in lending-service per Anfrage geprüft, die Regel selbst lebt aber konzeptionell hier).
- Value Objects:
EmailAddress, Mitgliedschafts-Tarif (MembershipTier: STANDARD, PREMIUM, STUDENT - Strategy Pattern für tarifabhängige Regeln, siehe Pattern-Katalog).
lending-service: Loan
- Invarianten: eine Ausleihe hat immer ein Fälligkeitsdatum > Ausleihdatum; eine zurückgegebene Ausleihe kann nicht erneut zurückgegeben werden; eine Verlängerung ist nur erlaubt, wenn keine Vormerkung durch eine andere Person vorliegt.
- Orchestriert eine Saga über mehrere Bounded Contexts hinweg (siehe Pattern-Katalog: Saga, Transactional Outbox).
reservation-service: Reservation
- Invarianten: FIFO-Reihenfolge der Warteliste pro Titel; ein Mitglied kann denselben Titel nicht doppelt vormerken.
fine-service: Fine
- Value Objects:
Money(siehecommon-domain.Money- kapseltBigDecimal+Currency, verhindertdouble-Rundungsfehler bei Geldbeträgen). - Invarianten: eine Gebühr wird genau einmal pro überfälligem Ausleihvorgang erzeugt
(Idempotenz über die
loanId, wichtig bei "at-least-once"-Zustellung von Kafka-Events).
notification-service
- Kein eigenes Aggregate - ein reiner Übersetzer/Ausführer (Event → Command, siehe ADR-0003). Bewusst fachlich "dünn", um zu zeigen, dass nicht jeder Service ein reiches Domainmodell braucht (Klassifikation nach Vaughn Vernon: "Supporting Subdomain" statt "Core Domain").
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.