Aber wenn nicht Microservices – was dann? Die wesentliche Alternative zu einem Microservices-System ist ein Monolith. Genau genommen gibt es verschiedene Arten von Monolithen (Abb. 1):
- Ein Deployment-Monolith kann technisch nur als Ganzes deployt werden. Gerade ältere Enterprise-Java-Systeme zählen zu dieser Kategorie.
- Bei einem Architekturmonolithen gibt es hingegen keine vernünftige Strukturierung des Systems in einzelne Module. Oder die Abhängigkeiten zwischen den Modulen sind so stark ausgeprägt, dass man einen Teil des Systems weder isoliert verstehen noch ändern kann. Eine andere Bezeichnung für diesen Architekturansatz ist „Big Ball of Mud“ [1]. Es gibt gute Indizien, dass er einer der populärsten Architekturansätze ist, weil man mit diesem Ansatz zumindest kurzfristig schnell Software entwickeln kann.
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Spring AI [4], das kürzlich das MCP integriert hat, bietet Entwicklerinnen und Entwicklern nun auf Java-Basis eine komfortable Möglichkeit, MCP-Server direkt in Spring-Boot-Anwendungen zu konsumieren und eigene MCP-Server zu implementieren.
Abb. 1: Microservices und die verschiedenen Spielarten von Monolithen
Es geht es also um zwei unterschiedliche Eigenschaften des Systems: Das Deployment und die Änderbarkeit. Ein gemeinsames Deployment kann sinnvoll sein, weil dazu eine weniger komplexe Infrastruktur notwendig ist – kein Kubernetes, nur eine Deployment Pipeline, einfachere Anforderungen bei Observability. Die Wahl eines Deployment-Monolithen kann daher eine sinnvolle Architekturentscheidung sein. Ein Architekturmonolith ist hingegen das Ergebnis, wenn man die Strukturierung der Software beispielsweise zugunsten von Investitionen in Features vernachlässigt.
Warum eigentlich Microservices?
Microservices sind ein Gegenentwurf zu Deployment-Monolithen. Typischerweise teilt man das System beispielsweise in einzelne Docker-Container auf, die eine Schnittstellen für ihre Funktionalitäten mit Technologien wie REST im Netz anbieten. So kann man technisch gesehen jeden dieser Microservices unabhängig voneinander deployen. Um tatsächlich unabhängig deployen zu können, muss es getrennte Deployment-Pipelines geben. Das ist nur möglich, wenn es beispielsweise auch unabhängige Tests gibt. So wird der Deployment-Prozess eines Deployment-Monolithen, der oft groß und komplex ist, durch viele kleine Deployment-Prozesse ersetzt, die einfacher sein sollten.
Das getrennte Deployment ist nicht der einzige Vorteil von Microservices. Es gibt weitere:
- Microservices können auch getrennt skaliert werden. Man kann mehrere Instanzen jedes einzelnen Microservice starten und so jeden Microservice an die individuelle Last anpassen.
- Der Absturz eines Prozesses führt nur zum Ausfall eines Microservice, die anderen laufen weiter. Bei einer geschickten Aufteilung des Systems bleiben die meisten Funktionalitäten erhalten. Microservices können also dabei helfen, die Resilience (Widerstandsfähigkeit) eines Systems zu verbessern.
- Auch zugunsten der Sicherheit kann die Aufteilung in Docker-Container Vorteile bringen. Zwischen den Containern können beispielsweise Firewalls eingezogen werden, sodass ein Angreifer mehr Schwierigkeiten hat, Kontrolle über größere Teile des Systems zu erlangen.
- Schließlich können unterschiedliche Microservices auf verschiedener Hardware laufen. o kann ein Teil des Systems, der beispielsweise GPUs benötigt, auf entsprechender Hochleistungshardware betrieben werden, während der restliche Systemteil auf weniger leistungsfähiger und damit kostengünstiger Hardware läuft.
Ein Argument für Microservices war, dass sie das Durchhalten einer Aufteilung einfacher machen. Man kann nicht einfach irgendeinen Bestandteil wie eine einzelne Klasse eines anderen Microservice nutzen, sondern nur die Schnittstelle, die beispielsweise über REST angeboten wird. In einem Deployment-Monolithen ist es hingegen leicht, auf irgendeine Klasse irgendwo im System zuzugreifen. Wenn das zunächst nicht geht, kann man auch schnell die Sichtbarkeit auf public umstellen – und schon kann man eine neue Abhängigkeit einbauen. So gehen die ursprünglich geplanten Strukturen verloren und am Ende steht ein Architekturmonolith. Microservices sollten also dieses Problem der Architekturmonolithen lösen, so die Hoffnung.
Microservices = bessere Strukturierung?
In der Realität sind aber „verteilte Monolithen“ (Distributed Monoliths) entstanden. Sie sind Architekturmonolithen, weil sie eine schlechte Strukturierung des Systems aufweisen. Die schlechte Struktur hat bei Microservices-Systemen die Auswirkung, dass Änderungen mehrere Microservices umfassen und die Microservices auch sehr viel miteinander kommunizieren. Durch die starken Abhängigkeiten ist das System schwer modifizierbar und hat eine schlechte Performance. Dafür kann es verschiedene Gründe geben:
- Der schon erwähnte Architekturmonolith bzw. Big Ball of Mud ist populär, weil man zunächst sehr einfach und schnell zu Ergebnissen kommt. Erst später tauchen die Probleme auf. So kann man mit einem Microservices-System sehr schnell bei einem verteilten Monolithen enden. Dazu kommt, dass in einem Microservices-System zwar die Änderungen in einem Microservice vielleicht einfacher sind – wenn man aber die Struktur des Gesamtsystems ändern will, muss dazu Code zwischen Docker-Containern verschoben werden. Das ist selbst dann aufwendig, wenn der Code aller Docker-Container mit derselben Programmiersprache geschrieben worden ist.
- Wenn man einen Microservice so definiert, das er einfach und schnell ersetzbar sein soll, endet man bei sehr vielen Microservices. So ergibt sich leicht ein unübersichtliches System mit starken Abhängigkeiten. Die Priorisierung der Ersetzbarkeit gegenüber einer guten Strukturierung des Gesamtsystems ist eigentlich kaum sinnvoll. Die Nachteile der schlechten Struktur wiegen die Vorteile der besseren Ersetzbarkeit einzelner Microservices vermutlich mehr als auf. Bei solchen Entscheidungen kann auch eine Rolle spielen, Microservices ‚richtig‘ umzusetzen – also im Sinne gängiger Definitionen. Manche davon betonen beispielsweise die Austauschbarkeit einzelner Services als zentrale Eigenschaft. Allerdings ist das bloße Einhalten einer Definition wenig zielführend, wenn dadurch eine insgesamt schlechte Architektur entsteht.
Tatsächlich entstehen in einigen Situationen sogar Systeme mit hunderten Microservices. Das macht den Betrieb noch komplizierter und das System wird unübersichtlich, was es noch schwieriger macht, das System zu verstehen und zu ändern. Die Unübersichtlichkeit ist eigentlich erstaunlich: Ein System mit Hunderten von Packages ist nicht zwingend chaotisch. Wenn die Fachlichkeit eine solche Menge an Code erfordert, dann gibt es Wege, das System änderbar und verstehbar zu machen. Packages können in Packages zusammengefasst werden. Und natürlich kann das System in mehrere Maven-Projekte oder JAR-Dateien aufgeteilt werden. So ergeben sich grobgranulare Module, von denen es wenigere gibt, sodass das System verstehbar und änderbar wird. Durch eine solche Hierarchisierung kann man also komplizierte Systeme dennoch verstehen und entwickeln.
Wenn also ein Microservices-System zu viele Microservices enthält und daher unübersichtlich ist, ist die Reduktion der Anzahl nur eine Option. Für das Verständnis kann eine Hierarchie hilfreich sein – beispielsweise Cluster von Microservices, zum Beispiel durch eine Namenskonvention. Eine andere höhere hierarchische Ebene sind Teams: Sie sind für mehrere Microservices zuständig und haben in gewisser Weise auch eine Schnittstelle, nämlich alle Funktionalitäten, die sie anderen Teams anbieten. Wenn man die Schnittstelle eines Clusters oder der Microservices eines Teams beispielsweise an einem API Gateway als konsolidierte Schnittstelle nach außen anbietet, wird sich eine solche grobgranulare Strukturierung auch in der umgesetzten Architektur erkennbar machen und die Nutzung der Funktionalität vereinfachen.
Da Microservices über das Netz kommunizieren, kann eine große Zahl an Microservices auch zu Netzwerk-Overhead und schlechter Performance führen. Das Problem kann nur gelöst werden, indem die Anzahl der Microservices reduziert und die Aufteilung so angepasst wird, dass fachliche Verantwortung idealerweise in genau einem Microservice implementiert sind.
Bessere Struktur – aber wie?
Zu komplexe Abhängigkeiten von Microservices und zu viele Microservices zeigen dennoch deutlich: Microservices führen nicht immer zu einer besseren Strukturierung von Systemen. Das sollte eigentlich nicht überraschen. Sie sind nur ein Ansatz, um die Bestandteile eines Systems anders zu implementieren – eine Alternative zu Java Packages oder JARs. Wenn man die Aufteilung des Systems nicht gut hinbekommt, sind die Konsequenzen bei einer Microservices-Aufteilung sogar schwerwiegender. Aus dieser Beobachtung haben sich mehrere Konsequenzen entwickelt:
- Sicher ist es eine häufige Schwäche von Deployment-Monolithen, dass ihre Strukturierung entweder zu wünschen übriglässt oder mit der Zeit schlechter wird. Ohne zusätzliche Maßnahmen ist es fast sicher, dass das System über die Zeit unwartbar wird. Werkzeuge zum Architekturmanagement [2] können aber Abhängigkeiten überwachen und vermeiden so, dass sie sich unbemerkt einschleichen. Außerdem wird die Struktur explizit und formal überprüfbar, was Diskussionen über dieses Thema vereinfacht. Wenn eine Regel durchbrochen wird, sollte man idealerweise über sie diskutieren: Entweder ist sie nicht sinnvoll oder unklar. In beiden Fällen ist Kommunikation zielführend.
- Im Java-Universum erlaubt beispielsweise ArchUnit [3], die Struktur eines Systems mit einem Unit-Test zu überprüfen. Spring Modulith [4] kann bei der Strukturierung von Spring-Anwendungen helfen. jMolecules [5] gibt Regeln vor, mit denen ein System entsprechend taktischem Domain-Driven Design strukturiert werden kann.
- Das Durchsetzen, Überprüfen und Weiterentwickeln der Strukturierung des Systems setzt voraus, dass man das System sinnvoll aufgeteilt hat. In diesem Zusammenhang ist ein neues Interesse an Modularisierung als grundlegendes Konzept zur Systemstrukturierung entstanden. Auch Domain-Driven Design (DDD) hat dabei an Relevanz gewonnen. Besonders die grobgranulare Modularisierung durch Bounded Contexts kann hier unterstützen – weniger das taktische DDD mit seiner feingranularen Aufteilung in einzelne Klassen.
Diese Entwicklung ist ein Fortschritt. So rücken grundsätzliche Herausforderungen in der Softwarearchitektur in den Fokus. Weil Themen wie die Strukturierung von Systemen so wichtig sind, ist das eine sehr gute Entwicklung. Scheinbar bedeutet das aber ein Scheitern von Microservices, denn sie haben anscheinend ihr Versprechen nicht eingelöst. Tatsächlich war es aber von Anfang an klar, dass Microservices nur dann Vorteile bringen, wenn es eine gute Aufteilung gibt. Und Microservices haben, wie schon erwähnt, einige Vorteile im Bereich Skalierung, Sicherheit, Resilience und der Flexibilität bezüglich der Hardware.
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Für solche Zwecke ist der Einsatz von Microservices also nach wie vor sinnvoll und es gibt kaum gute Alternativen. Oft ergibt sich ein Entwurf, der bestimmte Teile des Systems als eigene Docker-Container umsetzt, ganz natürlich aus bestimmten Anforderungen, zum Beispiel wenn man einen Teil des Systems isolieren will, der typischerweise unter hoher Last steht oder nicht besonders stabil ist.
Deployment
Es gibt noch einen weiteren Vorteil, den man eigentlich nur durch die Implementierung eines Systems mit Microservices erreichen kann: das unabhängige Deployment. In diesem Bereich gibt es nämlich kaum eine echte Alternative zu Microservices. Und die Vorteile können signifikant sein: Kleinere Deployment-Einheiten sind einfacher und mit weniger Risiko zu deployen. Und häufigere Deployments führen zu mehr Produktivität, weil Teams ihren gesamten Prozess beschleunigen und zuverlässiger machen müssen [6].
Aber auch beim Deployment wird nicht plötzlich alles gut, nur weil man Microservices nutzt: Die gesamte Deployment-Pipeline jedes Microservices muss unabhängig sein, wenn man die Microservices wirklich unabhängig deployen möchte. Das ist zum Beispiel nicht der Fall, wenn das gesamte System – also alle Microservices – gemeinsam End-to-End getestet werden sollen und diese Tests der hauptsächliche Mechanismus sind, um Fehler zu finden. Der Fokus auf Tests des Gesamtsystems kann gute Gründe haben. So kann es sein, dass es kaum sinnvoll ist, nur einen Teil des Systems zu testen, weil Fachlichkeiten nur im Zusammenspiel mehrerer Systeme funktionieren. Dann ist die schlechte Strukturierung des Systems aber die Quelle des Problems und müsste idealerweise beseitigt werden.
Consumer-driven Contract-Tests
Technisch lässt sich ein Integrationstest mehrerer Microservices durch Consumer-driven Contracts umsetzen [7] (Abb. 2). Statt die Microservices gemeinsam zu testen, ruft ein Microservice im Test eine Simulation der anderen Microservices auf, einen sogenannten Stub. So wird klar, welche Art von Interaktion der Microservices von anderen Microservices erwartet. Daraus wird ein Contract (Vertrag) generiert. Dieser Vertrag ist die Basis von Tests des aufgerufenen Microservices. So kann der aufrufende Microservice in seiner Continuous Delivery Pipeline einen formalen Vertrag erzeugen und der aufgerufene Microservice in seiner Continuous Delivery Pipeline mit diesem Vertrag getestet werden. So ist sichergestellt, dass beide Microservices zusammenarbeiten können, auch wenn sie nie zusammen in einer Umgebung liefen. Ein populäres Werkzeug für Consumer-driven Contract-Tests ist Pact [8], das solche Contract-Tests auch bei der Benutzung verschiedener Programmiersprachen in den Microservices unterstützt.
Abb. 2: Consumer-driven Contract-Tests können Integrationstests beseitigen
Organisation
Aber gemeinsame Tests sind nicht der einzige Grund, warum Microservices zusammen deployt werden. Es kann auch sein, dass ein Deployment einzelner Microservices untersagt wird, vielleicht weil ein Betriebsteam für das Management der Produktionsumgebung zuständig sein soll und nur alle Microservices gemeinsam deployen will. Bei der Technologieauswahl ist eine Einschränkung der technischen Möglichkeiten üblich. Rein technisch kann jeder Microservice in einer anderen Programmiersprache mit anderen Frameworks und Bibliotheken umgesetzt werden – und auch die Werkzeuge, beispielsweise für die Continuous Delivery Pipeline, können völlig unterschiedlich sein. Einige Projekte schränken daher die nutzbaren Technologien durch Regeln ein. Ein einheitlicher Technologiestack hat zweifellos Vorteile: Der Wissensaustausch zwischen den Teams ist einfacher und Menschen können einfacher zwischen den Teams wechseln. Auf der anderen Seite sind Teams dann nicht dazu in der Lage, die beste Technologie für die jeweilige Herausforderung zu nutzen.
Hier gibt es einen Widerspruch: Die bessere Austauschbarkeit von Wissen und Menschen kommt den Teams zugute – warum sollten sie sich gegen einen einheitlichen Stack entscheiden? Jedes Team kann Lösungen beispielsweise für die Continuous Delivery Pipeline einfach übernehmen und muss sich nicht selbst damit beschäftigen.
Am Ende ist ein wesentlicher Einflussfaktor dafür, wie viel man vorschreibt, Vertrauen: Wenn man die Entwickler:innen als verantwortungslose Spielkinder wahrnimmt, wird man ihnen mehr vorschreiben. Wenn man die Entwickler:innen als fähig und zuverlässig sieht, wird man ihnen mehr Freiheiten lassen. Übrigens kann es sein, dass Menschen mehr Verantwortung übernehmen, wenn man ihnen mehr Entscheidungskompetenzen gibt. Ein Mittel gegen als Spielkinder wahrgenommene Entwickler:innen kann also sein, ihnen mehr Freiheiten zu geben, damit sie daraufhin mehr Verantwortung übernehmen müssen.
Am Ende geht es hier also um organisatorische Themen und den menschlichen Faktor. Microservices können technische Freiheiten nur ermöglichen. Ob die Freiheiten wirklich umgesetzt werden, ist eine Frage, die organisatorisch entschieden werden muss.
Solche Freiheiten machen Teams autonomer, weil sie dann technische Entscheidungen unabhängig von anderen Teams oder Personen fällen können. Autonome Teams müssen weniger mit anderen kommunizieren und sollten daher produktiver sein – auch die Jobzufriedenheit wird dann höher sein. Agilität setzt auch auf autonome Teams. Da Agilität mittlerweile das typische Vorgehen in Projekten darstellt, müssten also die meisten Projekte in diese Richtung arbeiten. Dennoch setzen de facto viele Organisationen aber gerade nicht auf autonome Teams, auch wenn es Lippenbekenntnisse zu diesem Ideal gibt [9].
Fazit
Microservices sind nicht die endgültige Lösung für alle Architekturprobleme – genauso wenig wie alle anderen Architekturansätze auch. Dennoch haben sie eine wichtige Diskussion in Gang gesetzt:
- Microservices sind nur eine weitere Möglichkeit, Systeme aufzuteilen. Die Aufteilung und damit die Architektur werden nicht automatisch besser, nur weil man beispielsweise statt Java Packages nun Microservices nutzt.
- Daher rücken Strukturierung und Modularisierung stärker in den Fokus. Mit Ansätzen wie Domain-Driven Design wird die Orientierung an der Domäne wieder wichtiger. Diese Themen sind fundamental für Softwarearchitektur, aber auch für den Erfolg von Projekten, sodass sie nun zurecht wichtiger werden.
- Technische Vorteile wie getrennte Skalierung, getrenntes Deployment, mehr Resilience oder unabhängige Technologieentscheidungen sprechen nach wie vor für Microservices – und sind in einigen Fällen alternativlos.
- Schließlich wirft die möglicherweise größere technische Autonomie die Frage auf, wie viel Autonomie man den Teams wirklich zugestehen möchte.
Macht man also noch Microservices? Hoffentlich ja und hoffentlich nur da, wo es sinnvoll ist – aber das gilt für jeden Architekturansatz.
Links & Literatur
[1] Software Architektur im Stream zu Big Ball of Mud: https://software-architektur.tv/2023/03/31/folge159.html
[2] Software Architektur im Stream Folgen zu Architektur-Management-Werkzeugen: https://software-architektur.tv/tags.html#Architecture%20Management
[3] Software Architektur im Stream: „Peter Gafert zu ArchUnit“: https://software-architektur.tv/2021/04/09/folge55.html
[4] https://spring.io/projects/spring-modulith
[5] Software Architektur im Stream: „Taktisches Domain-Driven Design mit Java und jMolecules mit Oliver Drotbohm“: https://software-architektur.tv/2024/05/31/episode219.html
[6] Software Architektur im Stream: „Warum Continuous Delivery – Die DevOps Studie“: https://software-architektur.tv/2020/08/14/folge012.html
[7] https://martinfowler.com/articles/consumerDrivenContracts.html
[8] https://pact.io
[9] Software Architektur im Stream: „Autonome Teams – Wollen wir das wirklich?“: https://software-architektur.tv/2025/01/17/folge247.html