Start / Seminare / Domain-Driven Design in der Praxis

Modul

Repositories

5 Kapitel in diesem Modul-Video · Laufzeit

Vertonung mit synthetischer Stimme · Inhalte redaktionell verantwortet · © 2026 HECKER CONSULTING, alle Rechte vorbehalten

Transkript

Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.

DDD — Repositories

0:00 Wir haben ein Aggregat, wir haben eine Datenbank — es fehlt der Weg dazwischen. Genau den baut dieses Modul, und zwar mit einem Muster, dessen Anspruch erstaunlich hoch ist: Ein Repository soll das Aggregat so aussehen lassen, als läge es schon im Speicher. Der aufrufende Code sieht keine Session, keine Abfrage, keine Verbindung.

0:19 Dazu kommen zwei Themen, die im Alltag ständig auftauchen und selten sauber gelöst werden — der Umgang mit Stammdaten und die Trennung von Prüfen und Handeln. Es ist das letzte Modul im ersten Bounded Context; danach kommt ein zweiter dazu, und es wird interessant.

Repositories

0:35 Fünf Kapitel. Zuerst das Muster selbst mit seinen vier Regeln, dann die Basisklasse und die Frage, wie lange eine Session eigentlich leben soll. Danach das Thema, bei dem die meisten Fehler passieren — ein Aggregat wirklich vollständig zu speichern, wobei der Fehler fast nie im Code steht, sondern in der Abbildung. Dann die Stammdaten samt einem kleinen Muster namens Null- Objekt, das Ihnen an vielen Stellen Prüfungen erspart. Und zum Schluss die Trennung von Befehl und Abfrage.

Das Repository-Muster

1:03 Fangen wir bei der Idee an. Ein Repository ist kein Datenzugriffsobjekt mit schönerem Namen — der Unterschied liegt darin, was es nach außen zeigt und was es konsequent verbirgt. Der Unterschied liegt darin, was es nach außen zeigt und was es konsequent verbirgt — und daran misst sich, ob es gut gebaut ist. Ein Repository kapselt jede Kommunikation mit der Datenbank.

1:25 Der springende Punkt steht im zweiten Satz: Der aufrufende Code holt sich ein Domänenobjekt mit einem Aufruf und arbeitet damit, als hätte es immer schon im Speicher gelegen. Der Vergleich, der hier trägt, ist eine gut geführte Bibliothek — Sie nennen, was Sie brauchen, und bekommen es. Wie das Magazin organisiert ist, in welchem Regal es stand, ob es nachbestellt werden musste: nicht Ihr Thema.

1:48 Genau diese Illusion soll ein Repository erzeugen, und daran misst sich, ob es gut gebaut ist. Drei Zeilen, die zeigen, wie sich das anfühlt: Automaten holen, kaufen, speichern. Kein Session-Objekt, keine Abfrage, keine Transaktion in Sicht — der Aufrufer sieht keine Datenbank. Interessanter ist der zweite Teil. Wir suchen nach einem Fach und bekommen trotzdem den Automaten zurück, nicht das Fach.

2:13 Das sieht auf den ersten Blick nach einem Fehler aus, ist aber die Regel aus dem letzten Modul in Reinform: Ein Fach hat außerhalb des Aggregats keine Bedeutung. Sie dürfen danach suchen — Sie bekommen die Wurzel, an der es hängt. Die vier Regeln folgen alle aus dem Aggregat-Begriff, und die rechte Spalte sagt warum. Eines je Aggregat — weil die Wurzel die Einheit ist, die geladen und gesichert wird.

2:38 Nach außen nur Wurzeln, innere Teile gehen automatisch mit; es gibt keinen Grund, warum jemand von außen ein Fach einzeln speichern sollte. Die vierte Regel ist eine Namensregel mit Substanz: keine Speichertechnik im Namen, weil der Aufrufer nicht wissen soll, woher die Daten kommen. Und die Fußzeile räumt ein verbreitetes Missverständnis aus — ein Repository darf intern am ORM vorbei direkt abfragen, wenn es sich lohnt. Es bleibt trotzdem der einzige Weg.

3:05 Der erste Punkt ist der häufigste Fehler überhaupt: ein Repository je Entity, weil das so schön symmetrisch aussieht. Damit ist die Aggregatgrenze aufgehoben, noch bevor sie gewirkt hat. Der zweite ist die gleiche Grenzverletzung eine Ebene tiefer. Der dritte ist die Namensfalle — der Zusatz „SQL" verrät dem Aufrufer etwas, das ihn nichts angeht, und macht einen späteren Wechsel zur Umbenennungsaktion.

3:29 Und der vierte ist der, der sich in gewachsenen Systemen findet: Datenbankzugriffe an einer zweiten Stelle, weil es dort schneller ging. Ab da gibt es zwei Wahrheiten darüber, wie Daten geladen werden.

Die Repository-Basisklasse

3:42 Wie schon bei Entity und Value Object gibt es auch hier eine gemeinsame Basisklasse. Sie ist knapp — zwei Methoden — und schafft es trotzdem, eine der vier Regeln schon beim Übersetzen zu erzwingen. Zwei Methoden, mehr braucht sie nicht — und sie schafft es trotzdem, eine der vier Regeln schon beim Übersetzen zu erzwingen statt sie nur zu empfehlen.

4:02 Alle Repositories teilen dieselben Grundoperationen: eines nach Identifikator holen und eines speichern. Der interessante Teil ist der zweite Satz. Der Typparameter ist auf Wurzeln beschränkt — und damit ist die Regel „ein Repository je Aggregat" keine Konvention mehr, an die man sich erinnern muss, sondern eine Bedingung, die der Übersetzer prüft.

4:22 Wer versucht, ein Repository für ein Fach zu bauen, bekommt einen Fehler, bevor der Code überhaupt läuft. Das ist immer die bessere Sorte von Regel: eine, die man nicht brechen kann, statt einer, an die man denken muss. Die entscheidende Zeile ist die Typbeschränkung ganz oben — nur Aggregatwurzeln sind zugelassen. Der Rest ist bewusst unaufgeregt: eine Methode zum Holen über den Identifikator, eine zum Speichern, beide asynchron, beide sehr kurz.

4:50 Und dann die Fußzeile, die mir die liebste dieses Moduls ist: Löschen fehlt bewusst — was nicht gebraucht wird, wird nicht gebaut. Der Reflex, eine Basisklasse gleich mit dem vollständigen Satz von Operationen auszustatten, ist verständlich und teuer. Jede Methode, die niemand aufruft, muss trotzdem gepflegt, getestet und beim Umbau mitgezogen werden.

5:12 Hier zeigen sich zwei sehr unterschiedliche Betriebsarten, und der Unterschied hat handfeste Folgen. Im abgelösten Betrieb — etwa bei einer Desktop-Anwendung — öffnet jede Methode ihre eigene Session und schließt sie sofort wieder. Die Objekte leben danach ohne Verbindung weiter, und deshalb müssen sie vollständig geladen sein: Was fehlt, kann nicht mehr nachgeholt werden.

5:33 In einer Web-Anwendung ist es anders. Dort hält die Session die ganze Anfrage über, und sie wird ins Repository hineingereicht statt in jeder Methode neu erzeugt. Wer die eine Denkweise in die andere Umgebung mitnimmt, bekommt sehr schwer verständliche Fehler. Der erste und der zweite Punkt sind schon bekannt — falsche Granularität und Methoden auf Vorrat.

5:54 Der dritte ist ein Kapselungsbruch mit besonderer Tragweite: Wer die Session nach außen gibt, hat die ganze Arbeit dieses Kapitels zurückgenommen, denn ab da kann jeder direkt abfragen. Der vierte ist der klassische Laufzeitfehler im abgelösten Betrieb: Sie greifen auf eine Sammlung zu, das ORM will nachladen, die Verbindung ist längst zu.

6:13 Der Fehler erscheint weit entfernt von seiner Ursache, meist in der Oberfläche — und deshalb sucht man lange an der falschen Stelle.

Das Aggregat vollständig laden und speichern

6:21 Jetzt wird es praktisch heikel. Ein Aggregat vollständig zu speichern klingt banal, und der häufigste Fehler in diesem Bereich steht nicht im Code, sondern in der Abbildung — dort, wo niemand hinschaut. Denn das Aggregat wird trotzdem gespeichert, nur eben unvollständig: kein Fehler, keine Warnung, nur falsche Daten in der Datenbank.

6:41 Ein Aggregat wird als Ganzes geladen und als Ganzes gesichert. Für den Aufrufer heißt das: Er ruft einmal „speichern" auf, und die Fächer sind mit gespeichert — ohne dass er davon etwas weiß. Der letzte Halbsatz ist das Qualitätsmerkmal. Wenn der Aufrufer wissen muss, dass er die Fächer separat behandeln muss, ist die Kapselung gescheitert, auch wenn technisch alles funktioniert.

7:02 Der Punkt an dieser Stelle ist, dass diese Vollständigkeit nicht von selbst entsteht. Sie hängt an Einstellungen in der Abbildung, und die schweigen, wenn sie fehlen. Fünf Schritte, und der fünfte ist der, den ich Ihnen ans Herz legen möchte. Die ersten vier sind Handwerk: Tabellen anlegen, Value Objects einbetten, die Abbildung je Entity schreiben, die Sammlung an der Wurzel abbilden und die Kaskade setzen.

7:26 Beachten Sie im zweiten Schritt den Zusatz — Verweise auf andere Aggregate werden nicht nachgeladen, sonst holen Sie das halbe Modell in den Speicher. Und dann Schritt fünf: mit einem Test gegen die Datenbank prüfen, dass beide Seiten stimmen. Warum das entscheidend ist, sagt die Fußzeile: Die Wurzel wird auch ohne Kaskade korrekt gespeichert. Der Fehler ist also unsichtbar, solange Sie nur die Wurzel prüfen.

7:51 Zwei Einstellungen, und beide haben es in sich. Die Kaskade sorgt dafür, dass Änderungen an den Fächern mitgehen — ohne sie wird der Automat brav gespeichert und das geänderte Fach bleibt, wie es war. Kein Fehler, keine Warnung, nur falsche Daten. Und die zweite Einstellung schaltet das verzögerte Nachladen ab, was im abgelösten Betrieb notwendig ist: Was nicht mitgeladen wird, ist später nicht mehr erreichbar.

8:14 Diese beiden Zeilen sind ein gutes Beispiel dafür, wie folgenreich Konfiguration sein kann — der eigentliche Fehler steckt hier nicht im Code, sondern in dem, was nicht dasteht. Der erste Punkt ist die Kapselungsaufgabe: Der Aufrufer speichert die inneren Entities einzeln, weil es ohne Kaskade eben nicht anders ging. Das ist der typische Umweg, der aus einem Konfigurationsfehler entsteht.

8:37 Der zweite ist der Test, der zu wenig prüft — nur die Wurzel ansehen und den unveränderten Zustand der Teile übersehen. Der dritte ist das Nachladen bei verlorener Verbindung, das wir eben besprochen haben. Und der vierte ist der Speicherfresser: Verweise auf andere Aggregate mitladen, die ihrerseits Verweise haben — und plötzlich zieht ein einzelner Aufruf das halbe Modell in den Speicher.

Stammdaten und das Null-Objekt

9:00 Kommen wir zu einer Datensorte, die anders behandelt werden darf als der Rest — und zu einem kleinen Muster, das an vielen Stellen Prüfungen erspart und dem Zustand „leer" einen Namen gibt. Beides sind kleine Muster mit großer Wirkung — sie sparen Abfragen, sparen Prüfungen und geben einem Zustand einen Namen, der bisher keinen hatte.

9:19 Die drei Wertkarten sind Stammdaten: vorgegeben, selten geändert, vom Benutzer nicht anlegbar. Solche Daten darf das Modell fest kennen — als benannte, unveränderliche Felder statt als Ergebnis einer Abfrage. Das klingt zunächst nach genau dem, wovor wir sonst warnen. Der Unterschied liegt im Merkmal „vorgegeben": Diese Werte sind Teil der Domäne, nicht Teil des Datenbestands, den Nutzer erzeugen.

9:43 Der Gewinn ist erheblich — im Code steht ein sprechender Name statt einer Abfrage, und der Übersetzer prüft mit. Der Preis steht auf der Stolperstein-Folie, und den sollten Sie kennen. Drei benannte Instanzen und ein privater Konstruktor — damit ist sichergestellt, dass es genau diese drei gibt und niemand eine vierte erfindet.

10:03 Die interessanteste ist die dritte: der Eintrag „keine" mit der Nummer null. Das ist das Null-Objekt. Es steht für ein leeres Fach, und die Betonung liegt auf: es steht für ein leeres Fach, nicht für einen Fehler. Der Unterschied ist entscheidend. Ein leeres Fach ist ein völlig normaler Betriebszustand — nur eben einer, für den es bisher keinen Wert gab außer der Abwesenheit eines Werts. Jetzt hat er einen Namen.

10:28 Der praktische Gewinn steht im zweiten Punkt: Prüfungen auf fehlende Werte entfallen an jeder Stelle, die damit arbeitet. Sie kennen diese Prüfungen — sie stehen zu Dutzenden im Code, und eine davon fehlt immer. Wenn ein leeres Fach stattdessen eine gültige Wertkarte hält, gibt es nichts zu prüfen. Der dritte Punkt ist der, den ich für den wertvollsten halte: Der Zustand „leer" bekommt einen Namen und wird damit besprechbar — auch im Gespräch mit dem Fachbereich, der über Nichts schlecht reden kann.

10:56 Und dasselbe Muster funktioniert eine Ebene höher noch einmal, für den leeren Kartenstapel. Der erste Punkt ist der Preis, den ich angekündigt habe: Stammdaten stehen jetzt an zwei Orten — im Code und in der Datenbank —, und die können auseinanderlaufen. Deshalb ist der zweite Punkt die Gegenmaßnahme: die festen Felder gegen die Datenbank testen, sonst fällt die Abweichung erst spät und an einer unerwarteten Stelle auf.

11:20 Der dritte ist ein feiner, aber wichtiger: Das Null-Objekt gehört nicht in die Datenbank. Es ist ein Modellierungsmittel, kein Datensatz — sonst taucht es in jeder Auswertung als vierte Wertkarte auf. Und der vierte ist die Grenzverschiebung: veränderliche Daten als Stammdaten behandeln, weil es gerade praktisch ist. Diese Aufgabe setzt genau dort an, wo das Muster verwundbar ist.

11:42 Schreiben Sie einen Test, der jeden festen Wert im Modell mit dem Datenbestand vergleicht und fehlschlägt, sobald eine Seite geändert wird — egal welche. Damit haben Sie die Doppelpflege abgesichert, ohne sie abzuschaffen. Und beachten Sie den Hinweis, denn er enthält die Feinheit: Für das Null-Objekt gilt die umgekehrte Erwartung — es darf in der Datenbank gerade nicht stehen.

12:04 Der Test prüft also zweierlei zugleich, und genau diese Doppelrolle macht ihn zu einem guten Beispiel für einen Test, der eine Entwurfsentscheidung festhält.

Befehl und Abfrage trennen

12:13 Zum Abschluss ein Grundsatz, der weit über DDD hinausgeht und trotzdem selten konsequent angewandt wird. Es geht darum, Prüfen und Ausführen sauber auseinanderzuhalten — und darum, warum die naheliegenden Alternativen alle scheitern. Er kostet Sie eine zusätzliche Methode und erspart Ihnen dafür Ausnahmebehandlung an jeder Stelle, die den Vorgang anstoßen möchte.

12:35 Zwei Sätze, die man sich merken kann: Eine Methode, die den Zustand ändert, gibt nichts zurück. Eine Methode, die etwas zurückgibt, ändert nichts. Der Nutzen ist Verlässlichkeit — man kann eine Abfrage beliebig oft aufrufen, in einem Test, in einem Log, in einer Anzeige, ohne Nebenwirkungen zu fürchten. Der dritte Satz zieht daraus die praktische Konsequenz für unseren Fall: Wo eine Aktion Vorbedingungen hat, bekommt sie eine zweite Methode, die genau diese prüft.

13:03 Die Oberfläche kann dann fragen, bevor sie handelt — und den Kaufen-Knopf ausgrauen, statt eine Fehlermeldung anzuzeigen. Diese Folie ist im Grunde ein kleines Ausschlussverfahren. Die Prüfungen in die Oberfläche legen? Geht nicht, sie arbeiten mit Daten wie Vorrat und Stückelung, die dort nichts verloren haben. Aus dem Kauf einen Rückgabewert machen?

13:25 Dann vermischen sich Befehl und Abfrage, und Sie können nicht mehr prüfen, ohne zu kaufen. Ausnahmen für erwartbare Fälle werfen? Ein leeres Fach ist kein Ausnahmezustand, sondern Alltag — Ausnahmen für Alltag machen den Code unlesbar. Bleibt genau eine Möglichkeit: eine Abfrage, die prüft, und ein Befehl, der handelt. Diese Methode sammelt alle Vorbedingungen an einer Stelle: leeres Fach, zu wenig Geld, kein passendes Rückgeld.

13:52 Sie ändert nichts und gibt eine Begründung zurück — und die Konvention ist charmant: Eine leere Antwort heißt, der Kauf ist möglich. Damit kann die Oberfläche den Knopf steuern und den Grund anzeigen, ohne selbst etwas über Stückelung zu wissen. Und jetzt die wichtigste Feinheit, die in der Fußzeile steht: Der Kauf selbst behält seine Prüfungen. Die Abfrage ersetzt sie nicht.

14:14 Sonst hinge die Gültigkeit des Aggregats daran, dass jeder Aufrufer brav vorher fragt — und darauf darf sich ein Modell nie verlassen. Der erste Punkt hebt die Trennung direkt wieder auf: eine Abfrage, die nebenbei etwas ändert — ein Zähler, ein Zwischenspeicher, ein Protokolleintrag. Der zweite ist die Wartungsfalle: Prüfungen doppelt pflegen, statt die Abfrage im Befehl mitzubenutzen; spätestens bei der dritten Änderung driften beide auseinander.

14:40 Der dritte ist eine Schichtenfrage — Fehlertexte, die eigentlich in die Oberfläche gehören, in der Domäne festlegen. Spätestens bei der zweiten Sprache fällt Ihnen das auf die Füße. Und der vierte ist der Rückschritt: auf die Abfrage verzichten und den Aufrufer die Ausnahme abfangen lassen. Sie teilen eine Methode mit Vorbedingungen so auf, dass Prüfen und Ausführen getrennt aufrufbar sind.

15:03 Zwei Erfolgskriterien: Die prüfende Methode ändert nachweislich nichts am Zustand, und die Oberfläche kommt ohne Ausnahmebehandlung aus. Der Hinweis nennt einen sehr eleganten Test für das erste Kriterium — rufen Sie die Abfrage zweimal hintereinander auf und prüfen Sie, dass beide Male dasselbe herauskommt. Das ist eine erstaunlich wirksame Prüfung, weil sie jede versteckte Nebenwirkung auffliegen lässt, ohne dass Sie wissen müssen, welche Sie suchen.

Was du mitnimmst

15:29 Vier Punkte, und der zweite ist der praktisch teuerste. Ein Repository je Aggregat, und nach außen nur die Wurzel — das ist die Regel, aus der die anderen folgen. Die Kaskade entscheidet, ob das Aggregat wirklich vollständig gespeichert wird, und der Fehler ist unsichtbar, wenn Sie nur die Wurzel prüfen. Stammdaten dürfen im Modell stehen, solange Tests sie an ihre Quelle binden. Und: Wer den Zustand ändert, gibt nichts zurück — und umgekehrt.

15:56 Damit ist der erste Bounded Context abgeschlossen. Im nächsten Modul kommt ein zweiter dazu, und dann wird sich zeigen, was es wirklich heißt, dass ein Modell nur innerhalb seiner Grenze gilt.

Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →