Die Architektur von Software in C# ist kein starres Regelwerk, sondern ein dynamischer Prozess, der sich an Kontext, Team und Projektanforderungen orientiert. Doch genau diese Flexibilität macht es so herausfordernd, die richtige Struktur zu finden. Viele Entwickler:innen scheitern daran, dass sie Architekturprinzipien blind übernehmen – ohne zu hinterfragen, ob sie zum konkreten Anwendungsfall passen. Die beste Architektur ist daher keine festgelegte Methode, sondern eine maßgeschneiderte Kombination aus bewährten Mustern.
Domänenlogik als flache Funktionsliste organisieren
Ein zentrales Prinzip für stabile und wartbare Software ist die Umsetzung der Domänenlogik als flache Liste von Funktionen, wobei jede Funktion genau eine Verantwortung trägt. Egal, ob du dabei auf das Mediator-Pattern mit CreateUserCommand und CreateUserCommandHandler setzt oder klassische Services wie ICreateUserService mit einer einzelnen Methode CreateUser verwendest – die konkrete Implementierung ist zweitrangig. Entscheidend ist, dass jede Funktion einen klaren, isolierten Zweck erfüllt.
Diese Herangehensweise minimiert technischer Schulden langfristig, da sie:
- Die Komplexität reduziert, indem Abhängigkeiten zwischen Komponenten minimiert werden.
- Die Wartbarkeit erhöht, da Änderungen an einer Funktion keine Seiteneffekte in anderen Teilen des Systems auslösen.
- Die Testbarkeit verbessert, da jede Funktion unabhängig von anderen getestet werden kann.
In der Praxis hat sich gezeigt, dass selbst komplexe Domänen wie Finanzanwendungen mit dieser Struktur stabil bleiben – vorausgesetzt, die Funktionen bleiben auf einer Ebene und vermeiden verschachtelte Aufrufe.
CQRS: Schreib- und Leseoperationen sinnvoll trennen
Das Command Query Responsibility Segregation (CQRS)-Pattern wurde ursprünglich entwickelt, um Schreib- und Leseoperationen auf Infrastruktur-Ebene zu trennen. Die Idee: Bei den meisten Business-Anwendungen entfallen etwa 90 % der Operationen auf Lesezugriffe, während nur 10 % auf Schreibvorgänge entfallen. Dementsprechend lohnt es sich, für Lesezugriffe ein separates, optimiertes Datenbanksystem einzusetzen, während Schreiboperationen in einem anderen System verarbeitet werden.
Doch in der Realität ist diese Trennung selten notwendig. In Ländern wie Tschechien, mit einer Bevölkerung von rund 10,5 Millionen Menschen, gibt es kaum Anwendungen, die eine solche Infrastruktur rechtfertigen würden. Selbst bei großen Portalen wie seznam.cz, die hohe Traffic-Spitzen verarbeiten, ist der Nutzen fraglich. Die Herausforderungen wie Event-Erfassung, Synchronisation und Fehlerbehandlung überwiegen oft den Mehrwert.
Dennoch bleibt die Trennung von Lese- und Schreibfunktionen innerhalb der Domäne sinnvoll. Warum? Weil das Lesen von Daten genauso zur Geschäftslogik gehört wie das Schreiben. Eine Domäne sollte nicht nur Änderungen an Daten repräsentieren, sondern auch deren Abfrage und Darstellung. Wer die Domäne strikt auf Schreiboperationen beschränkt, ignoriert die tatsächlichen Anforderungen des Business.
Höherwertige Funktionen und die Strukturierung von Abhängigkeiten
Sobald die Domäne aus einer flachen Liste von Funktionen besteht, entsteht oft das Bedürfnis nach höherwertigen Funktionen, die mehrere dieser Grundfunktionen kombinieren. Diese entsprechen dem Saga-Pattern, bei dem eine übergeordnete Funktion mehrere untergeordnete Funktionen steuert, um einen komplexen Geschäftsprozess abzubilden.
Damit diese Architektur stabil bleibt, gelten zwei zentrale Regeln:
- Hierarchische Aufrufe sind erlaubt, aber nur in eine Richtung. Eine Funktion der Ebene 2 darf Funktionen der Ebene 1 aufrufen, eine Funktion der Ebene 3 nur Funktionen der Ebene 2 – niemals umgekehrt. Funktionen auf derselben Ebene dürfen sich nicht gegenseitig aufrufen. Dies verhindert zyklische Abhängigkeiten und macht die Architektur vorhersehbar.
- Höherwertige Funktionen bleiben domänenintern. Sie dürfen keine direkte Abhängigkeit zu externen Systemen haben, sondern rufen ausschließlich andere Domänenfunktionen auf. Externe Aufrufe (z. B. an APIs oder Datenbanken) bleiben den Grundfunktionen vorbehalten.
In der Praxis reichen oft zwei Ebenen aus, selbst in komplexen Domänen wie Finanzsystemen. Lediglich in extrem großen Monolithen (z. B. Reisebuchungssysteme) können höhere Ebenen notwendig sein. Doch selbst hier gilt: Je flacher die Hierarchie, desto einfacher ist die Wartung.
Warum der Monolith die beste Wahl für die Domäne ist
Viele Architekturratgeber empfehlen, die Domäne in Module oder sogar separate Microservices zu unterteilen. Doch in den meisten Fällen ist ein monolithischer Domänenkern die bessere Wahl – zumindest innerhalb einer einzelnen Anwendung oder eines API-Projekts.
Die Aufteilung der Domänenlogik in verschiedene .csproj-Dateien oder sogar Microservices sollte nur dann erfolgen, wenn:
- Ein klares neues Anwendungsgebiet entsteht, das eigenständig betrieben werden soll.
- Die neue Komponente klare Verantwortlichkeiten übernimmt, die nicht mehr Teil des ursprünglichen Systems sind.
Andernfalls führt die Aufteilung zu unnötiger Komplexität. Die Domäne sollte als Einheit gedacht werden, deren Funktionen eng miteinander verwoben sind. Erst wenn die Anwendung wächst und neue Verantwortlichkeiten entstehen, lohnt sich die Extraktion in separate Projekte.
Erlaubte Operationen: Validierung, Transformation und Abhängigkeiten
Jede Funktion in der Domäne darf nur drei Arten von Operationen durchführen:
- Validierung: Überprüfung, ob die übergebenen Daten oder der interne Zustand den Anforderungen entsprechen. Dies kann sowohl Eingabevalidierung (z. B. Formatprüfung) als auch Geschäftsregeln (z. B. „Kunde muss mindestens 18 Jahre alt sein“) umfassen.
- Transformation: Manipulation von Daten im Speicher, z. B. Umwandlung von Währungen, Berechnung von Steuern oder Anreicherung von Daten mit zusätzlichen Informationen. Wichtig: Die Transformation sollte keine externen Abhängigkeiten haben, sondern rein auf den übergebenen Daten operieren.
- Abhängigkeiten: Aufruf externer Systeme wie Datenbanken, APIs oder Dateisysteme. Diese Operationen sollten in separaten Funktionen gekapselt sein und nur über Schnittstellen (z. B. Repositories) erfolgen. Wichtig ist, dass die Domänenfunktionen selbst keine direkte Abhängigkeit zu konkreten Implementierungen haben.
Durch diese klare Trennung bleibt die Domäne testbar, austauschbar und unabhängig von technischen Details.
EF Core direkt nutzen: Warum Abstraktion oft unnötig ist
Ein häufiger Streitpunkt in C#-Projekten ist die Frage, ob der DbContext von Entity Framework Core (EF Core) abstrahiert werden sollte – etwa durch ein Repository-Pattern oder eine IUnitOfWork-Schnittstelle. Die Argumente pro Abstraktion lauten oft:
- „Wir könnten später die Datenbank wechseln.“
- „EF Core ist langsam und schwer zu testen.“
- „Die Clean Architecture verlangt Abstraktion."
Doch in der Realität sind diese Argumente meist überholt oder irrelevant:
- Datenbankwechsel sind extrem selten. Selbst wenn ein Projekt von MSSQL zu PostgreSQL migriert werden soll, ist der Aufwand enorm – es sei denn, die Datenbank war von Anfang an sauber designed. Eine Abstraktion ändert daran nichts.
- EF Core ist performant genug. Mit Methoden wie
.AsNoTracking()lassen sich Leseoperationen optimieren. Für Schreibvorgänge ist EF Core in den meisten Fällen ausreichend schnell.
- Unit of Work ist oft überflüssig. Die Idee,
DbSet.AddinIUserRepository.Addzu abstrahieren und dannIUnitOfWork.CommitstattDbContext.SaveChanges()aufzurufen, bringt wenig Mehrwert. Das YAGNI-Prinzip („You Aren’t Gonna Need It“) gilt hier: Die meisten Projekte benötigen keine solche Abstraktion.
Stattdessen sollte der DbContext direkt verwendet werden – er ist bereits eine ausreichende Abstraktion. Für Tests können Mocks oder In-Memory-Datenbanken (wie SQLite) eingesetzt werden, ohne dass eine zusätzliche Schicht nötig ist.
Fazit: Architektur folgt der Domäne, nicht umgekehrt
Die beste Architektur ist die, die sich an den tatsächlichen Anforderungen der Domäne orientiert – nicht an theoretischen Idealvorstellungen. Ob du Funktionen, Services oder MediatR verwendest, ist zweitrangig. Entscheidend ist, dass die Struktur:
- Einfach und verständlich bleibt.
- Flexibel genug ist, um Änderungen aufzunehmen.
- Stabil genug ist, um technischer Schulden zu vermeiden.
In Zukunft wird die künstliche Intelligenz zwar bei der Codegenerierung helfen, doch die strategische Entscheidung über die Architektur muss weiterhin der Mensch treffen. Denn Architektur ist kein technisches Problem – es ist ein menschliches Problem, das Teamdynamik, Unternehmenskultur und Geschäftsanforderungen berücksichtigen muss.
Wer diese Balance findet, wird langfristig stabilere und wartbarere Software entwickeln.
KI-Zusammenfassung
C# projelerinizde fonksiyon odaklı domain tasarımı, CQRS’in yenilikçi yaklaşımları ve EF Core’un sunduğu avantajları keşfedin. Kod mimarisini optimize etmek için ipuçları ve stratejiler.