Das Problem

Die Wahl der Zielarchitektur für ein zukünftiges Produkt kann herausfordernd sein, besonders wenn die Entscheidung zwischen zwei scheinbar gegensätzlichen Ansätzen liegt: Monolith und Microservices. Beide Architekturen haben ihre eigenen Vor- und Nachteile, aber die richtige Entscheidung kann erhebliche Auswirkungen auf die Skalierbarkeit, Wartbarkeit, das Deployment und letztlich den Erfolg des Produkts haben.

Heutzutage sorgen Schlagwörter wie Microservices und Event-Driven Architecture (EDA) oft für Verwirrung und führen potenzielle Anwender in die Irre. Getrennte Deployment-Einheiten und ereignisgesteuerte Reaktionen klingen großartig, oder?

In diesem Artikel möchte ich herausfinden, welcher Ansatz besser ist – und warum man die damit verbundenen Herausforderungen von Anfang an berücksichtigen sollte.

Microservices vs. Monolith.

Microservices sind ein Software-Architekturansatz, bei dem Anwendungen in kleine, unabhängig deploybare Dienste aufgeteilt werden, die über APIs miteinander kommunizieren. Jeder Microservice kann unabhängig entwickelt, getestet und bereitgestellt werden, was Agilität, Skalierbarkeit und Fehlertoleranz fördert.

Vorteile von Microservices:

Skalierbarkeit – Jeder Microservice kann unabhängig bereitgestellt und skaliert werden, was die Skalierbarkeit der gesamten Anwendung verbessert.
Resilienz – Wenn ein Microservice ausfällt, kann der Rest der Anwendung weiterhin funktionieren.
Flexibilität – Durch die unabhängige Entwicklung jedes Microservices können verschiedene Technologien und Entwicklungsansätze genutzt werden.

Nachteile von Microservices:

Komplexität – Die Aufteilung in mehrere Services erhöht den Verwaltungsaufwand.
Kommunikationsprobleme – Der Austausch zwischen Diensten kann Latenz und zusätzliche Komplexität verursachen.
Overhead – Mehr Dienste bedeuten höhere Kosten und eine längere Time-to-Market.

Wie du dir vielleicht schon gedacht hast, werde ich in diesem Artikel auf die Nachteile des Microservice-Ansatzes eingehen.

Das erste Problem: Komplexität

Komplexität ist ein Aspekt, den man von Anfang an berücksichtigen muss. Es klingt zunächst einfach:

Klingt machbar, oder? Doch in der Praxis kann genau diese Modularität zu Problemen führen.

Kommunikation zwischen Diensten – Der unsichtbare Flaschenhals

Stell dir vor, du und dein Team arbeitet an einem neuen Produkt – sei es ein vielversprechendes Startup oder ein internes Unternehmensprojekt. Die Entscheidung wurde getroffen, kleine, separat deploybare Einheiten zu verwenden, wobei jede kleine Domäne ihren eigenen Microservice erhält. Die API ist daher der einzige Weg, um eine Aktion auszulösen oder Daten zu aktualisieren bzw. abzurufen (gemäß Best Practices). Damit nähern wir uns langsam dem Problem der Inter-Service-Kommunikation.

(https://successive.cloud/what-is-service-mesh/ Service Mesh, Inter-Service communication)
Service Mesh, Inter-Service communication

Du solltest dich fragen, warum die API nicht von Anfang an definiert, auf ihre Eignung für alle überprüft und erst dann mit der Entwicklung der Services begonnen wird. Das wäre eine berechtigte Frage, denn jede Idee oder jedes Produkt muss überprüft, getestet und validiert werden.

Aber bedenke die Bedeutung von API (Application Programming Interface): Es handelt sich nicht nur um eine RESTful API, die synchron und HTTP-basiert ist, sondern auch um asynchrone Systeme wie Messaging-Systeme (z. B. Kafka, RabbitMQ) und Pub/Sub-Systeme (z. B. Redis). Dort definiert die Struktur der Events die vertraglichen Schnittstellen der API. Wenn sich also die API eines Messaging-Systems ändert, kann dies dazu führen, dass Verbraucher, die nicht auf dem neuesten Stand sind, nicht mehr richtig funktionieren.

Eine Lösung wäre die Einführung einer Versionierung der Nachrichten. Das bedeutet jedoch, dass andere Verbraucher, die neue Nachrichtenformate empfangen möchten, ebenfalls aktualisiert werden müssen. Für den Produzenten heißt das, dass er zwei verschiedene Nachrichtenformate (neu und alt) bereitstellen muss – was zu mehr Unsicherheit und potenziellen Problemen führen kann. Besonders der menschliche Faktor spielt hier eine Rolle, da komplexe Prozesse fehleranfällig sind.

Die Herausforderung, mehrere Versionen einer API zu verwalten, betrifft nicht nur asynchrone Systeme, sondern auch synchrone APIs (z. B. REST, RPC). Hier müssen verschiedene Versionen von Endpunkten unterstützt und gleichzeitig die OpenAPI-Dokumentation (z. B. Swagger) aktuell gehalten werden.

GraphQL bietet hier eine elegantere Lösung: Es erlaubt das Hinzufügen neuer Felder oder Resolver, ohne dass die bestehende Dokumentation angepasst werden muss. Dennoch entbindet es nicht von der Notwendigkeit, alle abhängigen Verbraucher zu aktualisieren, zusätzliche Abstimmungen vorzunehmen und weitere Meetings zu führen.

Eine mögliche Lösung könnte sein, alles innerhalb eines Teams in einem Monorepo zu entwickeln. Dadurch wären alle Änderungen direkt mit dem API-Anbieter und dem Client-Service abgestimmt. Doch auch hier müsste sorgfältig überprüft werden, welche Verbraucher betroffen sind, um Breaking Changes zu vermeiden – was ein hohes Risiko darstellt. Deshalb sollte ein automatisiertes End-to-End-Testing eingeführt werden, um die Kompatibilität der Schnittstellen sicherzustellen. Dies führt jedoch zu einer Verlängerung der Entwicklungszeit.

Aber bist du sicher, dass dieser zusätzliche Aufwand wirklich seinen Zweck erfüllt und das Ziel rechtfertigt?

Monolithische Architektur – Grundlagen und Herausforderungen

Die monolithische Architektur ist ein Software-Designstil, bei dem eine Anwendung als eine einzige, eng verbundene Einheit strukturiert wird (weiter unten erkläre ich, wie man sie weniger gekoppelt gestalten kann). Die gesamte Anwendung wird als ein einziges Paket bereitgestellt und deployt, wobei alle Komponenten denselben Codebestand und Datenspeicher gemeinsam nutzen.

Dieser Ansatz wird häufig für kleinere, weniger komplexe Anwendungen verwendet, die nur begrenzte Skalierbarkeit und Flexibilität erfordern.

Vorteile der monolithischen Architektur:

Einfachheit – Monolithische Architekturen sind oft leichter zu entwickeln und zu warten als Microservices, da es weniger Komponenten gibt, um die man sich kümmern muss.
Performance – Monolithen bieten häufig eine bessere Leistung, da weniger Overhead durch die Kommunikation zwischen Komponenten entsteht.
Einfacheres Testen – Da sich alles in einem einzigen Codebestand befindet, ist das Testen oft weniger komplex als bei einer Microservices-Architektur.

Nachteile der monolithischen Architektur:

Skalierbarkeit – Monolithen sind oft schwierig horizontal zu skalieren, da das Hinzufügen weiterer Server nicht immer zu besserer Performance führt.
Wartbarkeit – Mit zunehmender Komplexität wird die Wartung schwieriger, insbesondere wenn es keine klaren Strukturierungsregeln gibt oder Funktionen stark miteinander verknüpft sind.
Flexibilität – Monolithen sind schwerer an veränderte Anforderungen oder neue Technologien anzupassen (was sich direkt aus dem vorherigen Punkt ergibt).

👉 Die monolithische Architektur eignet sich gut für einfache Anwendungen, die wenig Skalierbarkeit und Flexibilität erfordern. Für größere, komplexere Systeme sind Microservices in der Regel besser geeignet.

Wie macht man einen Monolithen fit für große Unternehmen?

Die Hauptprobleme monolithischer Architekturen sind Skalierbarkeit, Wartbarkeit und Flexibilität. Doch warum ist das so?

Die Antwort liegt in zwei essenziellen Konzepten der Softwareentwicklung: Kopplung (Coupling) und Kohäsion (Cohesion).

🔗 Kopplung beschreibt, wie stark die einzelnen Komponenten eines Systems miteinander verbunden sind. Es gibt starke (tight) und lose (loose) Kopplung.
📌 Kohäsion bezieht sich darauf, wie eng die Elemente innerhalb eines Moduls zusammenarbeiten, um eine einzelne, klar definierte Aufgabe zu erfüllen. Hohe Kohäsion bedeutet, dass Komponenten eng auf eine Aufgabe fokussiert sind, während niedrige Kohäsion darauf hinweist, dass sie mehrere, oft unabhängige Aufgaben erfüllen.

👉 Hohe Kopplung und niedrige Kohäsion machen ein System schwer wartbar und komplex. Niedrige Kopplung und hohe Kohäsion erleichtern die Wartung und Skalierbarkeit.

Wir sind uns einig, dass hohe Kohäsion und lose Kopplung nach dieser Definition die besten Voraussetzungen sind, um die Kommunikation zwischen den Modulen zu verbessern und unser System flexibler und wartbarer zu machen.

Das Problem des „Distributed Monolith“

Ein häufiger Fehler ist der Übergang zu Microservices, ohne die Kopplung zwischen den Komponenten eines Monolithen zu reduzieren. Das führt oft zu einem „verteilten Monolithen“ (Distributed Monolith). Ein verteilter Monolith ist nicht das, was gesucht oder gebraucht wird. Ursprünglich beginnt der Übergang zu Microservices oft damit, dass neue Microservices aus einer monolithischen Anwendung heraus aufgebaut werden – mit der Idee, eine skalierbare Zielarchitektur zu schaffen. Doch in der Praxis führt dies häufig nur zu einem hochgradig verteilten, aber weiterhin eng gekoppelten System, das alle Nachteile beider Ansätze kombiniert.

Das Hauptproblem: Die Services bleiben stark voneinander abhängig und erfordern eine intensive Koordination, da sie keine echte funktionale Trennung aufweisen.

Im Allgemeinen ist der verteilte Monolith ein architektonisches Anti-Muster, das unbedingt vermieden werden sollte, wenn ein Monolith in Microservices aufgeteilt wird.

💡 Die richtige Vorgehensweise:
Zuerst die Kopplung reduzieren, dann genau analysieren, welche Komponenten in getrennte Dienste ausgelagert werden sollten.

Der „Loosely Coupled Monolith“ als optimale Lösung

Ein Loosely Coupled Monolith vereint die besten Eigenschaften von Monolithen und Microservices – er ist strukturiert, modular und flexibel, ohne die Komplexität einer vollständigen Microservices-Architektur.

🛠 Schnelle Anpassung von Modulschnittstellen – egal, ob über direkte API-Calls oder ereignisgesteuerte Mechanismen.
🛠 Getrennte Domänenmodule – Teams können unabhängig arbeiten, ohne sofort auf Microservices umzusteigen.
🛠 Leichter Wechsel zu Microservices – Falls erforderlich, kann der Monolith später sauber in einzelne Services zerlegt werden.

Damit ein Monolith gut strukturiert und modular bleibt, müssen einige wesentliche Prinzipien beachtet werden:

📌 SOLID-Prinzipien – Diese Best Practices helfen, den Code flexibel, unabhängig und wartbar zu halten.
📌 Domain-Driven Design (DDD) – Die Geschäftslogik sollte klar von der Infrastruktur getrennt sein.
📌 Hexagonale Architektur (Ports & Adapters) – Eine saubere Schichtung der Architektur hilft, die gleiche Geschäftslogik mit verschiedenen externen Systemen zu verbinden.
📌 Event-Driven Architecture (EDA) – Eine ereignisgesteuerte Architektur kann den Datenfluss verbessern und langfristig eine leichte Migration zu Microservices ermöglichen.

(Graphic created by the author Denys Dudarev)

Das obige Bild zeigt, wie es in Schichten und Domänenmodule aufgeteilt und nach Zuständigkeiten organisiert ist, die in Zukunft als dedizierte Dienste mit gut abgestimmten Schnittstellen dazwischen betrachtet werden können. Die Anwendungen innerhalb jedes Domänenmoduls stellen separate Anwendungsfälle dar, die im Rahmen der Domäne angewendet werden können. Die Hauptregel lautet, dass ein Anwendungsdienst nicht von einer anderen Anwendung aufgerufen werden darf, da die Anwendungsfälle unabhängig sein müssen und keine direkte Beteiligung verursachen dürfen.

Jedes Domänenmodul muss seine eigene Datenbank oder zumindest spezifische Tabellen in Bezug auf die Domäne erhalten. Das Hauptziel besteht darin, die Domänengrenzen zwischen den Domänen nicht zu überschreiten (z. B. keine Abfrage der mit einer anderen Domäne verbundenen Tabelle). Die Kommunikation muss über externe Schnittstellen erfolgen, wie Modulfassaden und Event-Handler

Modulare Architektur und Schichten

Eine geschichtete bzw. modular aufgebaute Architektur (die hexagonale Architektur ist hierbei die am stärksten entkoppelte) ist eine effektive Methode, um eine klare Trennung zwischen Geschäftslogik und Infrastruktur zu gewährleisten. Die Infrastruktur (z. B. Frameworks, Adapter, Datenbanken, Cache, externe APIs, Protokolle) kann flexibel angepasst werden, während die Geschäftslogik stabil bleibt und unverändert funktioniert.

Slice it graphic

Unter Geschäftslogik verstehe ich Domänenlogik und Anwendungslogik. Es gibt nur einen feinen Unterschied zwischen ihnen, aber er ist entscheidend.

Insgesamt können Domänendienste ähnlich wie Entitäten in Anwendungen verwendet werden, während Anwendungen eigenständige Anwendungsfälle sind und sich nicht gegenseitig aufrufen sollten.

Event-Driven Architecture – Warum sie im Monolith funktioniert

Ereignisse (Events) sind der Schlüssel zur zukünftigen Entwicklung. Die ereignisgesteuerte Architektur (Event-Driven Architecture, EDA) innerhalb eines Monolithen ist weit verbreitet, wenn Ereignisse als Reaktion auf Aktionen innerhalb einer Domäne ausgelöst werden und andere Komponenten entsprechend darauf reagieren, z.B.: Ein Kunde legt eine Bestellung in den Warenkorb → automatisch wird eine Bestätigungs-E-Mail versendet.

Laut Domain-Driven Design (DDD) sind Ereignisse fester Bestandteil einer Domäne und sollten genutzt werden, um Verhalten und Aktionen innerhalb von Entitäten nachzuverfolgen. Ereignisgesteuerte Kommunikation hat den Vorteil, dass sie keine direkten Abhängigkeiten zwischen Modulen schafft. Stattdessen basiert sie auf abstrakten Event-Verträgen, sodass kein externes Modul direkt aufgerufen werden muss.

Die Anwendung von EDA ermöglicht eine einfache Transformation der internen Kommunikation in ein externes Nachrichtensystem – z. B. über: Message Queues (z. B. RabbitMQ, Kafka), Pub/Sub-Systeme (z. B. Redis Pub/Sub)

Zum Schluss.

Zum Abschluss lohnt es sich, die eigene Sichtweise auf die monolithische Architektur zu überdenken und genau abzuwägen, wann eine Microservices-Architektur tatsächlich sinnvoll ist.

Ein Monolith ist eine gute Wahl, wenn noch Ideen evaluiert werden und die Schnittstellen nicht vollständig definiert sind. Gleichzeitig eignet sich das Microservices-Muster besser, wenn die Wartbarkeit der Anwendung verbessert und unabhängige Teams ihre eigenen Services verwalten sollen. In Unternehmen erfolgt dieser Übergang oft dann, wenn die Kommunikation zwischen den Domänen gut etabliert ist und der Monolith in kleinere, unabhängige Komponenten aufgeteilt werden kann.

Empfohlene Vorgehensweise:
📌 Einen Loosely Coupled Monolith anstreben, um die Anwendung entkoppelter und flexibler zu machen.
📌 Bewusst entscheiden, ob eine Migration zu Microservices notwendig ist oder ob ein gut strukturierter Monolith ausreicht.
📌 Eine ereignisgesteuerte Architektur (EDA) kann auch innerhalb eines Monolithen genutzt werden, sodass Systemereignisse verarbeitet und später nahtlos in ein externes Nachrichtensystem überführt werden können.

Monolithen werden oft als stark gekoppelt betrachtet – doch mit den richtigen Techniken lassen sie sich modularer, flexibler und besser organisiert gestalten. 🚀

-----------

FAQ: Monolith vs. Microservices – Welche Architektur passt besser?

1. Wie lässt sich zwischen Monolith und Microservices entscheiden?

Die Wahl hängt von den Anforderungen an Skalierbarkeit, Teamstruktur und langfristigen Zielen ab. Ein Monolith ist einfacher zu entwickeln und zu warten, insbesondere für Startups oder kleinere Projekte. Microservices eignen sich besser für größere Unternehmen, die mehr Flexibilität und unabhängige Teams benötigen.

2. Ist eine Migration zu Microservices immer der richtige Schritt?

Nicht unbedingt. Microservices sind beliebt, bringen jedoch auch höhere Komplexität und Kosten mit sich. Wenn ein bestehender Monolith die Geschäftsanforderungen erfüllt und keine Performance-Probleme hat, ist eine Migration möglicherweise nicht notwendig.

3. Welche geschäftlichen Risiken birgt die falsche Architektur?

Ein zu früher Wechsel zu Microservices kann zu hohem Entwicklungsaufwand führen, während ein zu langer Verbleib bei einem Monolithen die Skalierbarkeit und Agilität einschränken kann. Wachstumspläne und zukünftige Anforderungen sollten vor einer Entscheidung sorgfältig abgewogen werden.

4. Wie lässt sich die Architektur zukunftssicher gestalten?

Ein Loosely Coupled Monolith vereint die Vorteile beider Architekturen und ermöglicht eine schrittweise Transition zu Microservices, falls nötig. Die Ausrichtung der Architektur an den Geschäftszielen stellt langfristige Flexibilität sicher.

5. Wann sollte ein Monolith aufgebrochen werden?

Wenn Skalierungsprobleme entstehen, Deployments zum Flaschenhals werden oder verschiedene Geschäftsbereiche sich unabhängig entwickeln, kann der Schritt zu Microservices sinnvoll sein. Allerdings kann eine direkte Umstellung auf Microservices zu unnötiger Komplexität und verlängerten Entwicklungszeiten führen.

Ein bewährter Ansatz ist es, zunächst eine Refaktorierung in einen Loosely Coupled Monolith mit klar definierten Modulgrenzen vorzunehmen und innerhalb des Monolithen eine ereignisgesteuerte Architektur (EDA) zu etablieren. Dies erleichtert es, später schrittweise in separate Services zu übergehen.

👉 Noch unsicher? Lass uns reden und die richtige Strategie für dein Unternehmen finden! 🚀