Der Spickzettel zum Seminar Domain-Driven Design in der Praxis — für den Entwurfstermin, das Review oder den Moment, in dem jemand fragt, ob das jetzt eine Entity oder ein Value Object ist. Zum Lernen der Methode taugt er nicht; dafür ist das Seminar da. Jeder Abschnitt verweist auf sein Quellmodul.
Wann DDD trägt
DDD wirkt gegen fachliche Komplexität — und nur dagegen.
| Dominierendes Attribut | Typisches Projekt | DDD hilft? |
|---|---|---|
| Datenmenge | Auswertung großer Sensorströme | nein |
| Performance | Handelsplattform, Latenz in Millisekunden | nein |
| Fachliche Komplexität | ERP-System, Unternehmensprozesse | ja |
| Technische Komplexität | Steuersoftware für eingebettete Systeme | nein |
Die drei strategischen Muster
| Muster | Frage, die es beantwortet |
|---|---|
| Allgegenwärtige Sprache | Wie nennen Fachbereich und Code dieselbe Sache? |
| Bounded Context | Wo endet die Gültigkeit eines Modells? |
| Kerndomäne | Welcher Teil rechtfertigt die meiste Sorgfalt? |
Nicht jede Fachlogik ist Kerndomäne — Buchhaltung kauft man zu.
Modul: Einführung
Die Ringe der Onion-Architektur
| Ring | Enthält | Bauform Desktop / Web |
|---|---|---|
| Oberfläche | Bildschirme, Eingaben, Darstellung | Ansichten / Templates |
| Anwendungsservices | Abläufe, Transaktionen, Orchestrierung | ViewModels / Controller |
| Repositories, Factories, Domain Services | Zugriff, Erzeugung, übergreifende Regeln | dieselben |
| Kern | Entity, Value Object, Aggregate, Domain Event | dieselben |
Die Datenbank fehlt in der Aufzählung — sie steckt hinter den Repositories. Der Ring entscheidet über die Zuständigkeit, nicht das Framework: Ergibt eine Logik nur mit Blick auf den Bildschirm Sinn, gehört sie in den Anwendungsservice; gilt sie unabhängig von jeder Anzeige, in den Kern.
Module: Einführung · UI- und Persistenzschicht
Entity oder Value Object
| Merkmal | Entity | Value Object |
|---|---|---|
| Identität | eigene, über die ID | keine, nur der Wert |
| Änderbarkeit | veränderlich | unveränderlich |
| Eigenständigkeit | steht für sich | gehört immer zu einer Entity |
| Datenbank | eigene Tabelle | keine eigene Tabelle |
Drei Arten von Gleichheit
| Art | Gleich, wenn … | Gilt für |
|---|---|---|
| Referenz | beide auf dieselbe Stelle im Speicher zeigen | beide |
| Identifikator | die ID übereinstimmt | Entity |
| Struktur | alle Bestandteile übereinstimmen | Value Object |
Ein Value Object ändert man nicht, man erzeugt ein neues aus dem alten. Zwei frisch erzeugte Entities ohne ID sind nicht gleich — sie haben noch keine Identität.
Modul: Der erste Bounded Context
Aggregate zuschneiden
| Eigenschaft | Was daraus folgt |
|---|---|
| Begriffliches Ganzes | Die Teile ergeben einzeln wenig Sinn |
| Invarianten | Das Aggregat ist zu jedem Zeitpunkt gültig |
| Genau eine Wurzel | Von außen zeigt jeder Verweis auf sie |
| Einheit beim Laden und Speichern | Am Stück geholt, am Stück gesichert |
| Konsistenzgrenze | Innen sofort konsistent, darüber hinaus später |
Vier Fragen an einen Zuschnitt
| Frage | Antwort und Folge |
|---|---|
| Ergibt die Entity ohne die andere Sinn? | Ja: eigene Wurzel. Nein: Teil des Aggregats |
| Wie viele Entities stecken darin? | In der Praxis ein bis drei, selten mehr |
| Wie groß wird die Sammlung? | Ab etwa 30 Einträgen eigenes Aggregat daraus |
| Wie oft wird gleichzeitig geschrieben? | Je größer das Aggregat, desto mehr Konflikte |
Value Objects zählen bei der Größe nicht mit. Aggregate aus einer einzigen Entity sind völlig in Ordnung.
Modul: Aggregate im Bounded Context
Repositories
| Regel | Begründung |
|---|---|
| Eines je Aggregat | Die Wurzel ist die Einheit, die geladen und gesichert wird |
| Nach außen nur Wurzeln | Innere Entities bleiben hinter der Grenze |
| Innere Teile gehen mit | Niemand von außen speichert ein Teil einzeln |
| Name ohne Speichertechnik | Der Aufrufer soll nicht wissen, woher die Daten kommen |
Ein Repository darf am ORM vorbei direkt abfragen — es bleibt trotzdem der einzige Weg zur Datenbank. Was es zurückgibt, sagt der Rückgabetyp: entweder eine vollständige Entity mit haltenden Invarianten oder ehrliche Lesedaten für die Anzeige, aber nie eine halb gefüllte Entity.
Modul: Repositories
Persistenz ohne Verschmutzung des Kerns
Wo die Abbildung liegt
| Strategie | Bewertung |
|---|---|
| Externe Konfigurationsdateien | geschwätzig, brechen beim Umbenennen unbemerkt |
| Annotationen an den Klassen | knapp, aber Persistenzwissen wandert in den Kern |
| Eigene Mapping-Klassen | Kern bleibt frei, Umbenennen bleibt nachvollziehbar |
Wer die ID vergibt
| Strategie | Wie sie funktioniert | Preis |
|---|---|---|
| Datenbank vergibt | Schlüssel entsteht beim Einfügen | Rückfrage nötig, bricht das Bündeln |
| UUID | Client erzeugt den Schlüssel selbst | sperrig zu lesen, breitere Indizes |
| Hi/Lo | Client reserviert einen Block auf Vorrat | eigene Tabelle für die Blocknummern |
Die Wahl entscheidet, ob eine Entity schon vor dem Speichern identifizierbar ist — und damit, ob die Gleichheitsprüfung greift.
Was das ORM verlangt
| Zugeständnis | Warum das ORM es verlangt |
|---|---|
| Klassen nicht als final markieren | Stellvertreter erben von ihnen |
| Zugriffe nicht privat halten | der Stellvertreter muss sie überschreiben |
| Parameterloser Konstruktor | Objekte entstehen über Reflexion |
| Realen Typ statt Stellvertreter vergleichen | sonst ist eine Entity sich selbst ungleich |
Dazu die zwei Bausteine, die jedes ORM kennt: die Session (verfolgt Objekte, schreibt gebündelt zurück — kurzlebig, eine je Interaktion) und die Session-Factory (hält Konfiguration und Verbindungen — eine je Datenbank).
Modul: UI- und Persistenzschicht
Bounded Context und Context Map
| Eigenschaft | Was daraus folgt |
|---|---|
| Grenze der Fachsprache | Derselbe Name darf anderswo etwas anderes heißen |
| Eigene volle Schichtung | Jeder Kontext hat eigene Entities und Repositories |
| Ausgesprochene Beziehungen | Was zwischen ihnen läuft, steht in der Context Map |
Ein Bounded Context ist keine Schicht der Zwiebel — er hat eine eigene.
| Begriff | Raum | Wer ihn festlegt |
|---|---|---|
| Subdomäne | Problem: ein Teil der Aufgabe | Kunde und Fachexperten |
| Bounded Context | Lösung: ein Teil des Entwurfs | das Entwicklungsteam |
Beziehungen in der Karte
| Beziehung | Bedeutung |
|---|---|
| Shared Kernel | Beide teilen ein Stück Modell und pflegen es gemeinsam |
| Konformist | Einer fügt sich dem Modell des anderen |
| Schutzschicht | Einer übersetzt, um Begriffe des anderen fernzuhalten |
| Getrennte Wege | Keine Beziehung — die Kontexte wissen nichts voneinander |
Grade physischer Trennung
| Grad | Was getrennt ist | Preis |
|---|---|---|
| Ordner im selben Projekt | nur die Namensräume | Grenzen hält allein Disziplin |
| Eigene Module | Bauteile und Abhängigkeiten | mehr Bauaufwand |
| Eigene Auslieferung | Prozesse, Datenbanken, Quellcode | Netz, Betrieb, Versionierung |
Was zwischen Kontexten geteilt wird
| Codeart | Teilen? |
|---|---|
| Code mit Fachlogik | Nur wenn beide dasselbe darunter verstehen |
| Basisklassen der Domäne | Innerhalb eines Teams ja, über Teams hinweg nein |
| Werkzeug- und Hilfscode | Klein: kopieren. Groß und wertvoll: eigenes Projekt |
Modul: Der zweite Bounded Context
Domain Events
Ein Domain Event meldet eine fachliche Tatsache, keine Anweisung — deshalb Vergangenheitsform, nur einfache Datentypen, keine Entities oder Value Objects im Ereignis.
| Ereignis | Art | Fachlich bedeutsam? |
|---|---|---|
| Knopfdruck auf dem Bedienfeld | System | nein |
| Fenster geschlossen, Zeitgeber abgelaufen | System | nein |
| Waschgang gestartet und Karte belastet | Domäne | ja |
Erzeugen gehört der Entity, Zustellen der Infrastruktur: Die Wurzel sammelt ihre Ereignisse, zugestellt wird erst nach dem Speichern — vorher kann der Vorgang noch scheitern. Danach die Sammlung leeren. Der Behandler liegt beim Empfänger, nie beim Absender.
| Lage der Kontexte | Zustellung |
|---|---|
| Ein Prozess | über den Arbeitsspeicher, kein Transport nötig |
| Getrennte Prozesse | über einen Nachrichtenbus |
| Verlauf wird gebraucht | Ereignisse werden gespeichert und später gelesen |
Modul: Domain Events
Domain Service oder Anwendungsservice
| Merkmal | Domain Service | Anwendungsservice |
|---|---|---|
| Lage | innerhalb der Domänenschicht | außerhalb |
| Trägt Fachwissen | ja | nein |
| Spricht mit der Außenwelt | nein | ja |
| Beispiel | Karten auf Automaten verteilen | Eingaben prüfen, Aufruf anstoßen |
Beide sind zustandslos. Ein Domain Service ist fällig, wenn der Vorgang zur Domäne gehört, aber zu keiner einzelnen Entity — ein Fahrzeug wartet sich nicht selbst. Wer nur weiterreicht und nichts fachlich entscheidet, ist keiner.
Immer gültig oder später prüfen
| Frage | Immer gültig | Später prüfen |
|---|---|---|
| Wo wird geprüft | im Konstruktor und in jeder Änderung | in einer eigenen Prüfmethode |
| Was man bekommt | Jedes Objekt in der Hand ist gültig | alle Prüfungen an einer Stelle |
| Was es kostet | Prüfungen verteilen sich im Modell | Man muss ans Prüfen denken |
Eine verletzte Invariante ist ein Fehler im Programm, kein Eingabefehler.
Modul: Ausblick und Anti-Patterns
Anti-Patterns am Code erkennen
| Zeichen im Code | Deutet auf |
|---|---|
| Alle Felder öffentlich, Logik in Services | Anämisches Modell |
| Entity mit fachfremder Aufgabe | Fette Entity |
| Entity fragt selbst die Datenbank | Fette Entity |
| Invarianten lassen sich von außen brechen | beides |
Modul: Ausblick und Anti-Patterns
Typische Fallen
- DDD über eine CRUD-Maske legen — Aufwand ohne Gegenwert; ebenso der Einsatz gegen Performance- oder Datenmengenprobleme.
- Mit der Datenbank anfangen statt mit dem Modell, oder das Schema zuerst entwerfen und das Modell daran anpassen.
- Persistenzwissen im Kern — Tabellennamen in der Entity, Spaltennamen als Zeichenketten, oder der Kern, der selbst ein Repository aufruft.
- Fachlogik im Anwendungsservice, weil es dort schneller geht. Der Kern bleibt dann eine leere Hülle.
- Innere Entities nach außen reichen — über einen Getter oder über das Repository. Beides macht die Aggregatgrenze löchrig.
- Teile eines Aggregats einzeln speichern und es zerrissen zurücklassen.
- Ein Repository je Entity statt je Aggregat, oder Methoden ergänzen, die noch niemand braucht: Löschen, Suchen, Zählen.
- Ein Value Object mit ID-Feld ausstatten, weil das ORM es angeblich braucht — oder Felder aus dem Vergleich auslassen.
- Oberflächenereignisse zu Domain Events erklären und das Modell fluten; ebenso Ereignisse in Befehlsform benennen („Saldo erhöhen” statt „Saldo geändert”).
- Vor dem Speichern zustellen und bei einem Abbruch nichts zurücknehmen können; oder die Sammlung danach nicht leeren.
- Den geteilten Kern wachsen lassen, bis er die Kontextgrenze faktisch aufhebt. Die Context Map dazu nie wieder anfassen.
- Muster anwenden, ohne die Frage benennen zu können, die sie lösen — oder zu jedem Begriff das volle Satzmuster anlegen, ohne Bedarf zu prüfen.