Modulith statt Microservices — die Rückkehr des Modularen Monolithen
Nach einem Jahrzehnt Microservice-Hype kehrt der modulare Monolith — der Modulith — als ernstgenommene Architektur-Option zurück. Was Spring Modulith im Java-Universum etabliert hat, lässt sich in .NET 9 mit den Mitteln der Sprache und der Projektstruktur sauber nachbauen. Wann das funktioniert und wann nicht.
Das Wort „Modulith” hat in der DACH-Software-Architektur-Diskussion in den letzten 18 Monaten einen Bedeutungssprung gemacht. Bis 2023 war es eine seltene Wortschöpfung in Architektur-Workshops, eine Antwort auf die Frage „was, wenn Microservices nicht passen, aber wir trotzdem modular bleiben wollen”. Anfang 2026 ist es ein anerkannter Begriff — getragen vom Spring-Modulith-Projekt im Java-Universum, von einer Reihe einschlägiger Konferenzvorträge auf der NDC, .NET Conf und JavaLand, und nicht zuletzt von dem nüchtern werdenden Tonfall der Microservice-Diskussion selbst.
Die Kurzdefinition: Ein Modulith ist ein deploybarer Monolith mit klaren, kompiliererzwungenen Modul-Grenzen. Er teilt sich die Adressraum, die Laufzeit und die Datenbank-Transaktion — er teilt nicht die Code-Verantwortung. Das ist weniger spektakulär als Microservices, weniger reibungsfrei als ein klassischer Monolith und in einer überraschend großen Zahl von Szenarien die operativ vernünftigste Wahl. Die Frage, ob ein konkretes Projekt diesem Profil entspricht, ist die wesentliche Architektur-Entscheidung, die das Konzept abverlangt.
Die Ausgangslage: warum die Microservice-Diskussion müde wird
Microservices waren in den 2010er Jahren die Antwort auf zwei Schmerzpunkte. Erstens, die Skalierungsprobleme großer Monolithen — sowohl in der Last als auch in der Entwicklungs-Organisation. Zweitens, die Reibung großer Teams an einem gemeinsamen Codebase. Die Antwort war: zerlege den Monolithen entlang seiner Bounded Contexts, deploye die Teile unabhängig, lass jedes Team seinen Service eigenständig betreiben.
In der praktischen Bilanz von 2026 zeigt sich: Diese Antwort war für die ganz großen Organisationen (Netflix-Maßstab, Amazon-Maßstab) richtig und ist für die mittleren Organisationen (50 bis 500 Entwickler, ein bis fünf Produkte) oft die teuerste Lösung des falschen Problems gewesen. Die Reibung, die Microservices erzeugen — Netzwerk-Hops in jedem Aufruf, eventual consistency in jeder Domain-Operation, Distributed-Tracing als Voraussetzung statt Beilage, Deployment-Pipelines pro Service — ist signifikant. Sie ist gerechtfertigt, wenn der Lastgrund oder der Organisations-Grund sie erzwingt. Sie ist nicht gerechtfertigt, wenn die ursprüngliche Motivation „wir wollen modularen Code” war.
Der Modulith trennt diese beiden Anliegen. Modularer Code ist eine Code-Strukturierungs-Frage und kann innerhalb einer einzigen deploybaren Einheit gelöst werden. Operative Trennung ist eine Deployment- und Skalierungs-Frage und kann separat entschieden werden. Wenn beide Anliegen zusammenfallen — der Service muss aus Skalierungsgründen separat laufen UND der Code soll modular sein —, sind Microservices die richtige Wahl. Wenn nur das eine zutrifft, bietet der Modulith das, was nötig ist, ohne den Preis des anderen zu zahlen.
DDD-Bounded-Context als Modul-Trenn-Kriterium
Eric Evans’ Domain-Driven Design liefert das Vokabular, das die Modulith-Diskussion in der Praxis braucht. Der Bounded Context ist die natürliche Grenze, an der ein Modul beginnt und endet. Er ist im Original (Evans 2003) als die Zone der konsistenten Sprache definiert: Innerhalb des Bounded Contexts bedeutet ein Begriff (etwa „Kunde”) eine konkrete Sache mit einem konkreten Modell; an der Grenze des Bounded Contexts beginnt eine andere Sprache, in der „Kunde” möglicherweise etwas anderes meint.
Wer eine Domain-Modellierung mit Event Storming oder Context Mapping aufbaut, kommt fast immer mit drei bis acht Bounded Contexts heraus. Das sind die natürlichen Module der Anwendung — und genau dies sind die Schnitt-Linien des Modulithen. Ein Beispiel aus einem typischen E-Commerce-Domain:
- Catalog — Produktverwaltung, Verfügbarkeit, Kategorien
- Pricing — Preisstrategien, Rabatte, Kampagnen
- Cart — Warenkorb-Management, Session-Logik
- Ordering — Bestellannahme, Order-Lifecycle
- Fulfillment — Versand, Tracking, Retouren
- Billing — Rechnungsstellung, Zahlungsabwicklung
In der Microservice-Variante wären das sechs Services mit sechs Datenbanken, sechs Deployment-Pipelines, sechs Authentifizierungsstrecken. In der Modulith-Variante sind das sechs Code-Module in einer .NET-Solution, die alle auf dieselbe Datenbank (möglicherweise mit Schema-Trennung) zugreifen und in einem Prozess laufen.
Die DDD-Aggregat-Roots bleiben innerhalb der Modul-Grenzen. Wer aus dem Ordering-Modul ein Produkt aus dem Catalog-Modul referenzieren will, tut das über die explizite Modul-Schnittstelle — nicht über einen direkten Datenbank-Join, nicht über einen direkten Klassen-Zugriff. Diese Disziplin ist genau das, was Microservices über die Netzwerk-Grenze erzwingen. Der Modulith muss sie mit anderen Mitteln durchsetzen.
Spring Modulith als Referenzprojekt
Das Spring-Modulith-Projekt — gestartet 2022 von Oliver Drotbohm bei VMware, mittlerweile bei Broadcom — ist die einflussreichste konkrete Ausformulierung des Konzepts. Es bietet im Java-Umfeld drei Werkzeuge:
Erstens, Modul-Verifikation als Test: Eine Test-Klasse, die mit ApplicationModules.of(MyApplication.class).verify() prüft, ob die deklarierten Modul-Grenzen eingehalten sind. Ein Verstoß gegen die Modul-Grenzen — etwa der direkte Aufruf einer internen Klasse eines anderen Moduls — lässt diesen Test fehlschlagen.
Zweitens, Modul-Dokumentation als Generator: Ein Werkzeug, das aus dem Code automatisch C4-Modell-Diagramme der Modul-Topologie erzeugt — inklusive der Abhängigkeiten zwischen Modulen, die das Tooling zur Compile-Zeit erkannt hat.
Drittens, Event-basierte Modul-Kommunikation: Eine Konvention, dass Module ihre Kommunikation nicht über direkte Methodenaufrufe, sondern über Domain-Events abwickeln — was die Modul-Kopplung lockerer hält und einen klaren Wachstumspfad in Richtung Microservices definiert, falls das Modul später extrahiert werden soll.
Die Erkenntnis aus Spring Modulith, die für die .NET-Adaption wertvoll ist: Modulith-Disziplin braucht Werkzeug-Unterstützung. Ohne automatische Verifikation der Modul-Grenzen verrutschen sie in jedem mittelgroßen Team innerhalb von Wochen. Mit Verifikation halten sie sich.
.NET-Adaption: Project-Strukturen und Internal-Visibility-Boundaries
In .NET 9 gibt es zwei Wege, einen Modulith zu strukturieren. Beide sind im Einsatz, beide haben Vor- und Nachteile.
Variante A: Ein Projekt pro Modul, alle in einer Solution.
TraceCommerce.sln
├── TraceCommerce.Host/ (App-Entry, Composition Root)
├── TraceCommerce.Shared/ (gemeinsame Primitives, IDs, etc.)
├── TraceCommerce.Catalog/ (Bounded Context: Catalog)
│ ├── Domain/
│ ├── Application/
│ ├── Infrastructure/
│ └── Catalog.cs (Modul-API: public)
├── TraceCommerce.Pricing/
├── TraceCommerce.Cart/
├── TraceCommerce.Ordering/
├── TraceCommerce.Fulfillment/
└── TraceCommerce.Billing/
Jedes Modul ist ein eigenes C#-Projekt mit klar definierten public-APIs und internal-Implementierungs-Klassen. Die internal-Sichtbarkeit ist in .NET projektbezogen — das heißt, Klassen, die im TraceCommerce.Catalog-Projekt internal sind, können vom TraceCommerce.Ordering-Projekt nicht aufgerufen werden, auch nicht über Reflection (es sei denn, InternalsVisibleTo ist explizit gesetzt — was an dieser Stelle aktiv vermieden wird).
Die Modul-API liegt in einer public-Klasse oder einem public-Interface, das einzig die nach außen sichtbaren Operationen exponiert. Ein typisches Beispiel:
// TraceCommerce.Catalog/Catalog.cs (public API)
namespace TraceCommerce.Catalog;
public interface ICatalogModule
{
Task<ProductSnapshot?> GetProductAsync(ProductId id, CancellationToken ct);
Task<IReadOnlyList<ProductSnapshot>> SearchAsync(string query, CancellationToken ct);
}
public sealed record ProductSnapshot(
ProductId Id,
string Name,
decimal NetPrice,
string Currency);
Alles andere — die EF-Core-DbContext-Klasse, die Aggregat-Roots, die internen Services — bleibt internal. Ein Ordering-Modul, das ein Produkt abrufen will, ruft ICatalogModule.GetProductAsync auf. Es kennt die Domain-Entity Product aus dem Catalog-Modul nicht.
Variante B: Ein Projekt mit Ordner-Struktur und Architektur-Tests.
TraceCommerce/
├── Modules/
│ ├── Catalog/
│ ├── Pricing/
│ ├── Cart/
│ ├── Ordering/
│ ├── Fulfillment/
│ └── Billing/
├── Shared/
└── Host/
Die Modul-Grenzen sind hier nur per Konvention vorhanden — die internal-Sichtbarkeit greift nicht, weil alles im selben Projekt liegt. Die Disziplin wird über Architektur-Tests durchgesetzt:
[Fact]
public void OrderingModule_ShouldNotReferenceCatalogInternals()
{
var result = Types.InAssembly(typeof(OrderingModule).Assembly)
.That()
.ResideInNamespace("TraceCommerce.Modules.Ordering")
.ShouldNot()
.HaveDependencyOn("TraceCommerce.Modules.Catalog.Internal")
.GetResult();
result.IsSuccessful.Should().BeTrue();
}
Die Bibliothek NetArchTest (in Version 1.x, weiter gepflegt 2026) oder ArchUnitNET (Portierung der Java-ArchUnit-Library) bieten die nötige Abstraktion. Die Disziplin ist hier schwächer durchgesetzt — neue Entwickler übersehen die Konvention leichter — und die Tests müssen aktiv gepflegt werden, wenn Module umbenannt werden.
In den vier Modulith-Codebasen, die ich für diesen Beitrag durchgesehen habe, hat sich Variante A klar als die robustere Wahl etabliert. Der Compiler ist die strengste Disziplin, die ein Code-Base haben kann — und sie kommt kostenlos.
Modul-Kommunikation: Events vs. direkte Aufrufe
Die zweite Disziplin-Frage des Modulithen ist, wie Module untereinander kommunizieren. Zwei Stile sind in der Praxis verbreitet.
Direkte Aufrufe über Modul-Interfaces sind der pragmatische Weg. Modul A ruft die public-API von Modul B auf. Vorteil: synchron, einfach zu debuggen, in einer Transaktion. Nachteil: Modul A kennt Modul B namentlich — die Kopplung ist gerichtet, aber explizit.
Domain-Events sind der lose gekoppelte Weg. Modul A publiziert ein Event (OrderPlaced), Modul B subscribiert (Inventory-Reservierung) und Modul C ebenfalls (Billing-Vorbereitung). Modul A weiß nichts von B und C. Vorteil: lose Kopplung, klares Erweiterungs-Modell, Wachstumspfad in Richtung asynchrone Verarbeitung. Nachteil: schwerer zu debuggen, eventual consistency innerhalb des Monolithen, transaktionale Komplexität.
Die typische Modulith-Empfehlung ist eine Mischung: synchron-direkt für Read-Operationen, event-basiert für Write-Operationen mit Seiteneffekten in anderen Modulen. Das vermeidet die Latenz-Komplexität asynchroner Reads und nutzt den Modularitätsgewinn der Events dort, wo er wirklich zählt — bei der Erweiterung der Verarbeitungs-Kette.
In .NET 9 ist die Standard-Implementation für Domain-Events innerhalb eines Modulithen entweder MediatR (in der weiterhin gepflegten 12.x-Version, mit der bekannten Lizenz-Änderungsdiskussion) oder die handgeschriebene Variante mit einem in-process Event-Bus auf Basis von Channel<T> oder einem einfachen List-of-Handlers. Welcher Stil gewählt wird, ist eine Geschmacks-Frage — der entscheidende Punkt ist die Disziplin, dass Module nicht ihre internen Klassen über Reflection oder über InternalsVisibleTo aneinander preisgeben.
Wann der Modulith funktioniert
Der Modulith ist die richtige Antwort, wenn drei Bedingungen zusammenkommen.
Erstens, die Last passt auf einen Prozess. Ein Modulith ist ein einziger Deployment-Einheit. Wenn die Anwendung im Lastfall mit horizontaler Skalierung (mehrere Instanzen desselben Prozesses) und vernünftigem vertikalem Sizing auskommt, ist der Modulith operativ einfacher als Microservices.
Zweitens, die Datenbank passt auf eine Instanz. Der Modulith teilt sich die Datenbank (mit Schema-Trennung pro Modul, idealerweise). Wenn die Datenbank-Last auf einer einzelnen Instanz mit angemessener Hardware und Read-Replicas läuft, ist der Single-DB-Ansatz weiterhin tragfähig.
Drittens, das Team-Setup passt zur Code-Einheit. Wenn ein einziges Team (oder eine Hand voll Teams, die regelmäßig gemeinsam deployen) die Verantwortung trägt, ist der Modulith ohne Conway-Reibung machbar. Wenn die Organisation aus zehn unabhängigen Teams besteht, die jeweils ihren Release-Zyklus wollen, beginnt der Modulith-Ansatz zu reiben.
In den meisten mittelständischen .NET-Produkten — interne ERP-nahe Anwendungen, B2B-Plattformen, Vertikal-SaaS-Produkte — sind alle drei Bedingungen erfüllt. Das ist der Grund, warum die Modulith-Diskussion in der DACH-Welt gerade Fahrt aufnimmt: Sie löst die Reibung der letzten Jahre, ohne in die Reibung der vorletzten Jahre zurückzugehen.
Wann der Modulith nicht reicht
Drei Szenarien sind die ehrlichen Grenzen.
Extreme Lastunterschiede zwischen Modulen. Wenn das Search-Modul tausendfach mehr Last hat als das Reporting-Modul, ist die unabhängige Skalierung der Module ein echter Vorteil. Im Modulith skaliert man den gesamten Prozess — was bedeutet, dass das Reporting-Modul mitskaliert, obwohl es kaum Last sieht. Das ist Ressourcen-Verschwendung, wenn die Disparität groß ist.
Heterogene Tech-Stacks. Wenn ein Teil der Anwendung in Rust laufen muss (Performance-kritisch), ein anderer Teil in Node.js (Frontend-SSR mit React-Server-Components), ein dritter in Python (ML-Pipeline) — ist der Modulith strukturell ausgeschlossen. Microservices sind hier die natürliche Antwort.
Organisatorische Unabhängigkeit als Hauptziel. Wenn die Organisation aus zwanzig Teams besteht, die jeweils unabhängig deployen, eigene SLAs verantworten und ihre Tech-Wahl autonom treffen, ist der Modulith eine politische Sackgasse. Die operative Kopplung im Deployment-Zyklus widerspricht dem organisatorischen Modell.
Operative Reibung vs. Microservice-Overhead
Die ehrliche Bilanz lautet: Der Modulith trägt eine kleine, konstante operative Last — die Disziplin der Modul-Grenzen, die Architektur-Tests, die saubere Code-Organisation. Microservices tragen eine große, anwachsende operative Last — Netzwerk-Resilienz, Distributed Tracing, Service-Discovery, Schema-Evolution über Service-Grenzen, Compensating Transactions.
Für die mittelständische .NET-Welt, die mit 5 bis 50 Entwicklern an einem Produkt arbeitet, ist die Modulith-Last die kleinere Rechnung. Für die Großorganisationen mit drei- oder vierstelligen Entwicklerzahlen ist die Microservice-Last die unvermeidbare. Die historische Microservice-Welle hat den Großorganisationen-Werkzeugkasten in die mittelständische Welt exportiert — und das hat in vielen Fällen die Gesamtkomplexität erhöht, ohne das ursprüngliche Problem (modularer Code) zu lösen, das einfacher zu lösen gewesen wäre.
Der Modulith ist die Rückkehr zur passenden Antwort. Robert Martins „Clean Architecture” (im weiteren Sinn) hat über Jahre die Disziplin der Schichten-Trennung innerhalb eines Monolithen propagiert — der Modulith ergänzt diese Vertikal-Trennung der Schichten um eine Horizontal-Trennung der Bounded Contexts. Beide Disziplinen lassen sich kombinieren: Innerhalb jedes Moduls sind Domain, Application, Infrastructure als Schichten getrennt; zwischen Modulen ist die Bounded-Context-Grenze die Schnittlinie.
Schluss: die nüchterne Antwort
Der Modulith ist 2026 keine Mode-Diskussion mehr, sondern eine etablierte Architektur-Option, die in den meisten neuen .NET-9-Projekten der mittelständischen DACH-Welt die ernstgenommene Default-Wahl ist. Die Java-Welt mit Spring Modulith ist der Vorreiter, .NET zieht mit etwa 18 Monaten Verspätung nach — die Werkzeuglandschaft (NetArchTest, ArchUnitNET, Roslyn-Analyzer für Modul-Sichtbarkeit) ist 2026 ausgereift genug, um die Disziplin praktikabel zu machen.
Die Diskussion in der Architektur-Community hat damit den Bogen geschlagen, den jede Software-Architektur-Mode irgendwann macht: vom monolithischen Ausgangspunkt über die radikale Zerlegung zurück zu einer differenzierten Antwort, die die ursprünglichen Anliegen beantwortet, ohne die neuen Probleme zu importieren. Modulith ist nicht der Ersatz für Microservices — er ist die richtige Antwort auf eine bestimmte Klasse von Problemen, die Microservices in den 2010er Jahren als ihre Klasse vereinnahmt hatten, ohne sie tatsächlich besser zu lösen.
Wer im Mai 2026 ein neues Produkt aufsetzt, sollte mit einem Modulith anfangen. Wer ein Microservice-Setup unterhält, das die Reibung nicht rechtfertigt, sollte den ehrlichen Refactor in Erwägung ziehen — der Weg vom Microservice-Set zurück zu einem Modulith ist organisatorisch schwierig, technisch aber gut beschritten. Die nächsten 18 Monate werden zeigen, wie viele der bestehenden Microservice-Topologien diesen Rückweg gehen — und wie viele die operative Last weiterhin zahlen, weil die Rückkehr politisch teurer ist als der Status quo.