Invarianten im Domain Model: Was .NET-Entwickler richtig machen — und was KMUs davon übernehmen sollten

Milan Jovanović erklärt, warum Geschäftsregeln ins Domain Model gehören, nicht in Handler und Validator. Aus der KMU-Beratungspraxis: was davon sofort hilft — und was zuerst fehlt.

⚙ KI-Entwurf Dieser Beitrag wurde automatisiert generiert und nicht redaktionell freigegeben. Behandelt ihn entsprechend.

Im letzten Quartal saß ich bei einem Maschinenbauer — 85 Mitarbeitende, C#-Eigenentwicklung seit 2014. Aufgabe: Audit vor dem Onboarding eines neuen Entwicklers. Die Frage, die ich als erstes stelle, ist immer dieselbe: "Zeig mir, wo geregelt ist, dass ein Lieferschein nach Versand nicht mehr geändert werden kann."

Was folgte, war ein 20-minütiges Grep-Abenteuer durch Handler, Services und Validators. Die Regel tauchte dreimal auf — zweimal korrekt implementiert, einmal nicht. Welche Version für welchen Endpoint gilt? Unklar. Der Entwickler, der das gebaut hatte, war seit einem Jahr weg. Das ist kein Einzelfall. Das ist der Normalzustand.


Was Milan Jovanović sagt

Milan beschreibt das Konzept der Invarianten im Domain Model: Regeln, die für ein Objekt immer gelten müssen — nicht nur beim Speichern, nicht nur wenn der richtige Validator zufällig auf dem Aufrufpfad liegt, sondern immer. An einem C#-Beispiel (Course, Lesson, Money) zeigt er drei konkrete Techniken: private Konstruktoren mit Static Factory Methods, die einen ungültigen Zustand strukturell ausschließen; Methoden statt Setter für State Transitions mit Result<T>-Rückgabe; und die Aggregate Root als einziger Eingang für Änderungen an zusammengehörenden Entitäten. Das Gegenmodell — das "anemic model" mit public Settern und ausgelagerter Validierung — lässt Regeln unweigerlich auseinanderdriften, sobald ein zweiter Endpoint hinzukommt.


Wie das im KMU wirklich landet

Das anemic model ist in 80 % der Mittelstands-Codebases, die ich sehe, die Realität. Nicht aus Bösartigkeit, sondern weil "erst schnell, dann richtig" selten den zweiten Schritt macht. Und Milans Diagnose ist präzise.

Aber sein Artikel richtet sich implizit an Teams mit DDD-Erfahrung und einer stabilen Architektur-Konvention. Das trifft auf einen Zwei-Personen-Dev-Shop in einem 120-Mann-Betrieb selten zu. Drei Punkte, die im Artikel fehlen:

1. Teamgröße und Lernkurve. Result<T>, Value Objects, Aggregate Roots — das ist ein signifikantes Umdenken. Ich habe Projekte gesehen, wo das Pattern halbherzig eingeführt wurde: private Konstruktoren ja, aber der Rest des Teams hat drei Monate lang drumherum gecodet und sich per Reflection direkten Zugriff erzwungen. Das ist schlimmer als das anemic model. Ein Pattern, das nur einer im Team versteht, macht die Codebase nicht sicherer. Es macht sie schwerer übergebbar.

2. Migrationskosten auf Bestandscode. Milan zeigt, wie man es richtig baut. Er zeigt nicht, was passiert, wenn 80.000 Zeilen Bestandscode auf dem anemic model beruhen. Im KMU ist Greenfield die Ausnahme. Die Frage ist immer: Wie viel Umbau kann ich mir leisten, ohne dass der laufende Betrieb kippt? Ein partielles Refactoring — neue Klassen richtig, alte Klassen unverändert — erzeugt in der Praxis oft mehr Verwirrung als der Status quo.

3. ORM-Reibung. Entity Framework und private Konstruktoren laufen zusammen, aber nicht ohne Aufwand. Owned Types, Shadow Properties, Backing Fields — das erfordert jemanden, der die EF-Konfiguration wirklich beherrscht. In vielen KMUs ist der Datenbankentwickler eine andere Person als der C#-Entwickler. Der Mapping-Layer wird zum Streitpunkt, und am Ende hat jemand den privaten Konstruktor wieder auf internal gesetzt, damit EF aufhört zu meckern.


Was davon sofort hilft — und was zuerst kommen muss

Milans wichtigste Aussage ist nicht das Muster selbst, sondern der Grundsatz dahinter: Die Klasse macht Versprechen, oder sie macht keine. Das ist ohne DDD-Vollausbau umsetzbar.

Für neue Features: immer. Wenn du eine neue Entity schreibst, gib ihr einen Konstruktor, der die Pflichtfelder erzwingt. Keine public Setter für Felder, die nach der Erstellung nicht mehr änderbar sein sollen. Das kostet 20 Minuten Mehraufwand und spart Stunden an Debugging — ich habe das in genug Post-Mortems nachgemessen.

Für Bestandscode: selektiv. Identifiziere die zwei oder drei Regeln, die am häufigsten falsch angewendet werden — die, die du im Code-Review immer wieder kommentierst. Kapsle genau diese. Nicht alles auf einmal. Ein Rule-by-Rule-Ansatz ist langsamer, aber er überlebt den Alltag.

Result<T> vs. Exceptions: Milan bevorzugt Result<T>. Das ist sauber, aber es erfordert Konsequenz im gesamten Stack. Wenn dein Team damit nicht vertraut ist, fang mit Exceptions an und migriere später. Ein halbherzig eingesetztes Result<T> — hier ja, dort nein, und niemand weiß warum — macht den Code schwerer lesbar, nicht leichter.

Aber das eigentlich Erste: Bevor du anfängst zu refactoren, braucht ihr eine Liste der Geschäftsregeln, die wirklich nie verletzt werden dürfen. Nicht als Kommentar im Code. Nicht im Confluence-Wiki. Als explizite, auffindbare Aussage, die jeder im Team kennt.

In einem Mandat haben wir dafür eine einfache Tabelle angelegt: Entität, Regel, wo sie durchgesetzt wird, Testabdeckung ja oder nein. Vier Stunden Arbeit. Das Ergebnis: 30 % der kritischen Regeln hatten keine oder unvollständige Tests. Das war ernüchternd — und gleichzeitig der wertvollste Schritt vor jedem Refactoring. Erst danach hat es Sinn, darüber nachzudenken, ob die Regeln in Factory Methods, in Methoden wie Publish() oder in einer Kombination landen sollen.

Domain-Modeling nach Milan ist das richtige Ziel. Aber das KMU, das dort hinwill, braucht einen Migrationspfad — und der beginnt nicht im Code, sondern im Gespräch darüber, welche Regeln überhaupt existieren.


Der vollständige Original-Beitrag von Milan Jovanović: What Invariants Are (and Why a Domain Model Is the Best Place to Enforce Them)


Du willst wissen, welche Regeln in deiner Codebase gerade niemand durchsetzt — und wo das als nächstes knallt? Dann buch dir 30 Minuten ohne Agenda:

30 Min Klartext-Sparring

— Bernhard