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.
Dieses Thema als Schulung für Ihr Team
Dieser Beitrag erklärt das Thema. Damit Ihr Team es danach auch anwendet, gibt es Domain-Driven Design in der Praxis als Schulung — an Ihrem eigenen Code, mit den Fragen, die ein Text nicht beantwortet. Sie wählen die Module, wir bauen daraus ein Programm.
2 Tage·ab 900 EUR netto pro Tag (bis 3 Teilnehmende) ·Termin nach Vereinbarung
Als Team-Schulung anfragenZum Seminar Domain-Driven Design in der Praxis →