Waren Microservices noch vor einigen Jahren der große Hoffnungsträger, stehen Microservices mittlerweile im Ruf, komplexe verteilte Systeme zu sein, von denen man besser ganz die Finger lässt. Wer aber heute glaubt, dass Monolithen die Lösung sind, hat mit diesem Ansatz vielleicht andere Erfahrungen gemacht. Es ist aber nicht unüblich, dass Monolithen Stunden kompilieren, eine Viertelstunde oder mehr zum Starten benötigen oder nur über das Wochenende deployt werden können. Ebenso ist die Modularisierung der Monolithen oft so schlecht, dass selbst scheinbar einfache Änderungen sehr aufwendig sind. Die Konsequenz aus der Kritik an Microservices kann wohl kaum ein Rückfall in diese Zeiten sein.

Microservices? Monolithen? Module!

Große Systeme sind immer in grob granulare Module aufgeteilt. Monolithen können beispielsweise in Java Packages oder Maven-Projekte unterteilt sein. Microservices stellen „nur“ eine andere Art der Modularisierung von System dar. Wenn die Unterteilung in Module suboptimal ist, hilft einem die Implementierungen der Module nicht weiter, weder als Java Package oder Maven-Projekt, noch als Microservices. Daher ist die Frage nach Monolithen oder Microservices nicht die entscheidende, sondern es muss um Module und die Unterteilung in Module gehen.

Dazu lohnt sich ein Blick auf die Herkunft des Begriffes Modul. Module sind ein Konzept, um die Entwicklung von Software auf größere Organisationen zu skalieren. Software-Projekte entstehen nahezu ausschließlich in Teams. Es ist praktisch ausgeschlossen, dass jede Entwickler:in das gesamte System detailliert versteht. Also muss es ein Konzept geben, mit dem Entwickler:innen das System mit begrenztem Wissen weiterentwickeln können. Hier kommen Module ins Spiel: Entwickler:innen sollen nur jeweils einzelne Module verstehen müssen, um sie zu ändern.

Module sind also nicht für Computer, sondern ein Werkzeug für Entwickler:innen, um das Verständnis des Codes und die Änderbarkeit großer Systeme zu verbessern. Wenn wir unbeschränkte geistige Kapazitäten hätten, bräuchten wir keine Module. Daher stellt sich bei der Aufteilung in Module die Frage, wie Menschen sich komplexe Systeme und typischerweise Wissen erschließen und strukturieren. Das sollte bei der Aufteilung in Module Beachtung finden.

Information Hiding

Es gibt aber Konstruktionsmechanismen für die Aufteilung von Systemen in Module, die so fundamental sind, dass man ihnen immer folgen sollte. Ein solches Konzept ist Information Hiding (Abb. 1). Grundlage ist die These, dass Entwickler:innen jede Information, die ein Modul zur Verfügung stellt, auch ausnutzen. Neben der Schnittstelle können dazu unter anderem Datenstrukturen gehören. Dieser Effekt wäre im Programm-Code erkennbar: Der Code nutzt bestimmte Teile des Moduls wie Methoden oder Instanz-Variablen. Es können aber auch Informationen ausgenutzt werden, die im Programm-Code nicht so einfach erkennbar sind. Um Performance-Anforderungen gerecht zu werden, können Entwickler:innen Informationen über das Laufzeitverhalten ausnutzen, die sie durch entsprechende Tests herausfinden. So treffen andere Teile des Systems Annahmen über Module – und das erschwert das Ändern der Module. Wenn andere Teile des Systems eine Instanz-Variable, eine Methode oder die Performance eines Moduls ausnutzen, führt eine Änderung dieser Elemente dazu, dass die Teile des Systems auch geändert werden müssen, die sich auf diese Elemente verlassen.

Abb. 1: Information Hiding: Nur bestimmte Informationen dringen nach draußen
Abb. 1: Information Hiding: Nur bestimmte Informationen dringen nach draußen

Die Lösung ist, möglichst viele Informationen zu verstecken – eben Information Hiding. So werden zum Beispiel Instanz-Variablen eigentlich nie exponiert und ebenso nur der Teil der Methoden, die public sind. Gegen das Ausnützen der Performance kann man hingegen kaum etwas tun. Durch die Reduktion der Informationen sind Module einfacher änderbar, da andere Teile des Systems weniger Informationen ausnutzen können und so Änderungen weniger Rücksicht auf andere Teile und ihre Annahmen nehmen müssen. Auch beim Information Hiding gilt, wie schon bei Modulen, dass es nicht so sehr um Computer geht, sondern um Menschen und das Begrenzen von Wissen. Während Modularisierung das Wissen begrenzt, das notwendig ist, um Code zu ändern, begrenzt Information Hiding das Wissen, das sich verbreitet und Änderungen erschwert, weil es durch Änderungen gegebenenfalls invalidiert wird.

Konkret kann Information Hiding durch Schnittstellen umgesetzt werden: Eine Klasse exponiert öffentliche Methoden. Nur eine Änderung an diesen Methoden hat einen Einfluss auf andere Teile des Systems und muss abgestimmt werden. Private Methoden und Datenstrukturen können einfach geändert werden, weil sie außerhalb der Klasse nicht genutzt werden können. Ein Bankkonto als Klasse beispielsweise sollte eine Möglichkeit bieten, den Kontostand auszugeben. Durch Information Hiding können Entwickler:innen das Bankkonto aber von einer Speicherung des Kontostands zu einer ad-hoc Berechnung des Kontostands aus den Konto-Transaktionen umstellen. Das wäre nicht möglich, wenn der Kontostand als Instanz-Variable direkt exponiert wird. Was für Instanz-Variablen und Klassen gilt, gilt auch für Microservices und Datenbanken: Wie ein Microservice Daten in einer Datenbank speichert, sollte nicht nach draußen exponiert werden, weil es sonst nicht änderbar ist.

Viele Entwickler:innen beachten diese Konzepte, aber die Hintergründe sind oft unklar. Aber gerade das Verständnis für die Begründungen ist notwendig, um Modularisierung erfolgreich umzusetzen. Dazu ist es wertvoll, sich ein Ziel wie Information Hiding zu vergegenwärtigen und dann dieses Ziel mit geeigneten Maßnahmen zu erreichen, statt bestimmte Ansätze ohne tieferes Verständnis zu verfolgen.

Unabhängige Module: Unmöglich!

Oft hört man den Ruf nach „unabhängigen“ oder „entkoppelten“ Modulen. Das ist aber ein Widerspruch: Module sind Teile eines Systems. Es kann daher keine unabhängigen oder entkoppelte Module geben, weil sie dann kein System mehr ergeben. Sie wären vollständig voneinander isoliert. Jede Beziehung zu anderen Modulen wäre eine Abhängigkeit und eine Kopplung an andere Module. Ohne solche Beziehungen kann aber niemals ein vollständiges System entstehen.

Dennoch lohnt ein Blick auf Abhängigkeiten. Erstrebenswert ist eine lose Kopplung (Abb. 2). Eine Änderung sollte idealerweise nur ein Modul umfassen. Allerdings kann es natürlich passieren, dass Änderungen auch andere Module beeinflussen. Das sollte aber die Ausnahme sein und die notwendigen Änderungen sollten so gering wie möglich sein. Die Änderung pflanzt sich durch die lose Kopplung eben nicht unvermindert auf die anderen Module fort. Das schon diskutierte Information Hiding ist ein Konzept, das wie diskutiert zur losen Kopplung beiträgt.

Abb. 2: Lose Kopplung: Eine Änderung hat Einfluss auf ein Modul, aber nur wenig Einfluss darüber hinaus.
Abb. 2: Lose Kopplung: Eine Änderung hat Einfluss auf ein Modul, aber nur wenig Einfluss darüber hinaus.

Ob die Kopplung der Module lose ist oder nicht, hängt von der Art der Änderungen ab. Der Informatik-Pionier Parnas plädierte 1971 dafür, Module so zu entwerfen, dass technische Entscheidungen in einem Modul versteckt sind. In seinem Beispiel ist nur ein Modul für die Speicherung der Daten zuständig, statt diesen Aspekt in allen Modulen umzusetzen. Er überprüfte diese Aufteilung dann durch verschiedene Änderungsszenarien, die jeweils technische Änderungen wie beispielsweise eine andere Art der Datenspeicherung umfassten. Für diese Änderungen waren die Module lose gekoppelt: Die Änderungen beeinflussen nur ein Modul oder einige wenige Module.

Dieses Konzept der „Einkapselung“ technischer Entscheidungen ist auch heute noch weitgehend bekannt und den meisten in Fleisch und Blut übergegangen. Selbstverständlich ist das Konzept nach wie vor sinnvoll, denn Code mit bestimmten technischen Belangen über das System zu verteilen, macht nicht nur Änderungen komplizierter, sondern das System ist dann auch schwieriger zu verstehen. Einfache Verständlichkeit ist aber eine Voraussetzung für die Änderbarkeit eines Systems.

In den 50 Jahren seit dem Erscheinen des Papers hat sich die Welt aber weiterentwickelt. Das Beispiel aus Parnas Paper diskutiert einen Algorithmus, der auf dem Zerschneiden und Sortieren von Strings basiert. Heutzutage steht man kaum vor der Herausforderung, für solche Funktionalitäten Module zu entwickeln. Und selbst wenn: eine effektive Modularisierung auf dieser Ebene trägt kaum zum Erfolg eines Projekts bei.

Heutzutage ist Modularisierung oft eine Herausforderung auf einer anderen Ebene: Es geht um größere Module. Und die typischen Änderungen sind nicht mehr technischer Natur, sondern fachlicher. Daher sollte sich die Modularisierung nach fachlicher Gegebenheit richten und insbesondere die grob-granulare Modularisierung auch betrachtet werden. Domain-driven Design kann dabei hilfreich sein.

Wegen der Unterstützung für fachliche Änderung sollte sich die Aufteilung der Module nach den Funktionalitäten richten. Logischerweise gelten auch für grob granulare Module immer noch Regeln wie Information Hiding. Welche Daten für die jeweiligen Funktionalitäten notwendig sind, sollte in den Modulen verborgen sein. Viele Funktionalitäten werden dabei Geschäftsobjekte wie Kunden oder Produkte benötigen. Aber unterschiedliche Funktionalitäten nutzen unterschiedliche Informationen über diese Geschäftsobjekte: Wenn ein Kunde ein Produkt bezahlt, benötigt man dazu den Preis des Produkts und die Bezahlmöglichkeiten den Kunden. Wenn man das Produkt zum Kunden liefern will, interessiert das Gewicht des Produkts und die Lieferadresse des Kunden. Ein Schnitt nach Funktionalitäten mit entsprechenden Ausschnitten der Daten wird eher zu einer losen Kopplung führen als ein Schnitt nach Daten. Änderungen bei der Lieferung oder Bezahlung beeinflussen wahrscheinlich nur ein Modul.

Wenn die Aufteilung nach Daten wie Produkt oder Kunde erfolgen würde, sähe das Bild ganz anders aus: Die Daten, wie Kunde oder Produkt, werden schließlich an vielen Stellen genutzt, sodass Änderungen an den Funktionalitäten, wie Lieferung oder Bezahlung, vermutlich oft auf die Datenmodule durschlagen würden.

Eine Aufteilung nach Funktionalitäten ist auf verschiedenen Ebenen die Basis einer guten Architektur: CRC-Karten (Class – Resonsibility – Collaboration) dienen dazu, ein objektorientiertes System in feingranulare Klassen (Class) aufzuteilen. Wesentlich sind dabei die Responsibilities (Zuständigkeiten) und Collaboration (Zusammenarbeit) mit anderen Klassen. Ein Bounded Context Canvas stellt eine Beschreibung für ein grobgranulares fachliches Modul aus dem Domain-driven Design dar und fokussiert ähnlich wie CRC-Karten auf die Beziehungen zu anderen Bounded Context, eine Beschreibung und die fachlichen Termini. So stehen also keine Daten, sondern Funktionalitäten und Beziehungen im Mittelpunkt der Aufteilung – und zwar auf verschiedenen Ebenen.

Zwischenergebnis

Wenn wir mit den bisher diskutierten Ansätzen ein System aufteilen würden, hätten wir nun verschieden grob granulare fachliche Module. Sie hätte Abhängigkeiten, aber eine lose Kopplung, vorrangig bei fachlichen Änderungen. Sie würden Funktionalitäten exponieren, aber die Umsetzung und die notwendigen Daten wegen Information Hiding verbergen. Diese Aufteilung ist zwar essenziell, aber noch nicht ausreichend, um das System tatsächlich umzusetzen. Schließlich kann ein Bounded Context als grob granulares Modul technisch völlig unterschiedlich umgesetzt werden: Ein Microservices, ein Maven Projekt oder Java Packages sind nur einige der zahlreichen Möglichkeiten. Und die Frage nach der Implementierung stellt sich bei jedem Bounded Context oder anderen grob granularen Modul erneut. Sicher müssen die Module eine Schnittstelle haben, aber das lässt sich mit einer Vielzahl von Ansätzen umsetzen: Außerhalb von Packages können nur public Java-Klassen genutzt werden. Mit einer Facade-Klasse kann das komplexe interne Verhalten eines grob-granularen Moduls versteckt werden und nur bestimmte Operationen überhaupt nach draußen exponiert werden. So können Java Packages oder Maven Module Schnittstellen exponieren. Microservices hingegen haben eine REST-Schnittstelle und setzen so dasselbe Konzept technisch anders um.

Microservices?

Wenn man lediglich Konzepte wie Information Hiding und lose Kopplung ansetzt, ist eine klare Entscheidung für einen der Modularisierungsansätze kaum zu treffen, denn sie setzten die Konzepte beide um, nur eben unterschiedlich. Microservices haben aber weitere Eigenschaften, die man grob in den Bereich Information Hiding einordnen kann: Für Nutzer eines Microservice ist nicht zu erkennen, in welcher Programmiersprache der Microservice implementiert ist. Bei einem Maven Module muss es zumindest eine Sprache sein, die Java Bytecode erzeugt. Außerdem kann ein Microservice neu deployt werden, ohne dass darüber Informationen nach außen dringen, wenn man geschickt vorgeht. Auch die Anzahl der Instanzen ist nach außen nicht erkennbar. So bieten Microservices mehr Möglichkeiten, Änderungen vorzunehmen. Das sind insbesondere technische Vorteile: Deployments, Änderungen der Programmiersprachen oder Skalierung eines Microservice sind möglich, ohne dass andere Module beeinflusst sind. Das erhöht den Spielraum der Personen, die für einen Microservice zuständig sind.

Auf der anderen Seite sind Microservices ein verteiltes System, sodass die Konsistenz der Daten, die Zuverlässigkeit der Kommunikation mit anderen Modulen und Transaktionen als zusätzliche Herausforderungen dazu kommen. Wie so viele Entscheidungen ist also auch die Entscheidung für Microservices ein Trade-Off.

In gewisser Weise haben Microservices ein höheres Risiko, aber auch größere Chancen: Wenn die Modularisierung nicht besonders gut ist, wird man mit Microservices zusätzlich zum hohen Aufwand bei der Implementierung auch Herausforderungen bei Konsistenz oder Performance wegen Netzwerk-Kommunikation haben. Wenn die Modularisierung hingegen gelungen ist, dann können Microservices nicht nur weitgehend unabhängig geändert werden, sondern auch skaliert, deployt – und sogar die Programmiersprache der Implementierung lässt sich ändern.

Absurd ist es natürlich, wenn man Microservices als Modul-Implementierung nutzt, aber die Vorteile durch organisatorische Maßnahmen unterbindet, wie etwa das Deployment zentral koordiniert oder alle Technologie-Entscheidungen zentralisiert. Vielleicht gibt es dennoch genügend Vorteile für die Entscheidung zugunsten von Microservices und vielleicht gibt es auch gute Gründe für die Einschränkungen – nachfragen und nachdenken sollte man dennoch.

Ob der Trade-Off, den Microservices repräsentieren, sinnvoll ist, kann man für jedes Modul erneut entscheiden. Bounded Contexts sollten etwa fachlich so entkoppelt sein, dass es kaum Herausforderungen bezüglich Konsistenz oder Transaktionen geben sollte. Domain-driven Design kennt Aggregates als die Bausteine, die für Konsistenz und Transaktion verantwortlich sind. Sie sind Bestandteile eines Bounded Context, sodass es zwischen Bounded Context keine Herausforderungen für Transaktionen und Konsistenz geben sollte. Wenn das doch der Fall ist, kann man die grob granularen Module statt als Microservice eben als Maven Module oder Java Packages mit einer gemeinsamen Datenbank umsetzen – und dann sind Probleme bezüglich Transaktionen und Konsistenz relativ einfach zu lösen.

Monolithen

Eine Entscheidung gegen Microservices ist also eine Architektur-Entscheidung, wie viele andere auch – und sollte nicht ideologisch, sondern basierend auf den zu erwartenden Vor- und Nachteilen getroffen werden. Wenn die Entscheidung gegen Microservices ausfällt, gibt es dennoch eine Herausforderung: Die Grenze von einem Microservices zu einem anderen zu überschreiten ist aufwendig. Es sind unterschiedliche Projekte, Docker Container und möglicherweise sogar Programmiersprachen. Ein Zugriff ist ebenfalls nur über die Schnittstelle möglich. In einem Monolithen hingegen ist es viel einfacher, irgendwelche Klassen aus anderen Modulen zu nutzen. Früher oder später führen Entwickler:innen dann irgendwelche Abhängigkeiten ein und so entsteht ein System, bei dem von einer Reduktion der Abhängigkeiten auf definierte Schnittstellen, wie Information Hiding es vorsieht, nicht mehr sprechen kann. So nimmt das verteilte Wissen immer weiter zu.

Dagegen kann man sich allerdings schützen, indem man die Abhängigkeiten aktiv managt und nur bestimmte Abhängigkeiten zulässt. Dazu gibt es zahlreiche Werkzeuge. Ohne ein aktives Management der Abhängigkeiten wird der Monolith aber seine Struktur früher oder später verlieren. Es scheint so, dass diese Erkenntnis sich erst in letzter Zeit durch Microservices und Domain-driven Design so richtig durchsetzt, obwohl die Werkzeuge schon länger vorhanden sind. Ein besseres Management der Modularisierung in Monolithen ist ein grundlegender Fortschritt, der die langfristige Wartbarkeit von Monolithen erheblich positiv beeinflussen kann.

Rightsize Monoliths!

Ebenso sollte es mittlerweile klar sein, dass Kompilierungszeiten im Stundenbereich und Startzeiten im Minutenbereich nicht mehr akzeptabel sind, wie sie zumindest früher bei Monolithen vorgekommen sind. Moderne Continuous Delivery Pipelines, die ein System automatisch kompilieren und testen, wären für einen solchen Monolithen kaum umsetzbar, weil die Pipeline viel zu langsam wäre und mit der geringen Geschwindigkeit von Deployment und Test oft auch eine hohe Komplexität einhergeht. Ein Monolith dieser Größe sollte daher in kleinere Systeme aufgeteilt werden – die man aber wohl nicht unbedingt Microservices nennen würde. Die Strukturierung in verschiedene Systeme war auch schon vor Microservices eine Möglichkeit für die grob granulare Modularisierung, der aber Monolithen-Monster nicht verhindern konnte. Microservices haben gezeigt, was in diesem Bereich möglich ist.

Fazit

Die Diskussion zu Monolithen oder Microservices verstellt die Sicht auf die Frage nach der Modularisierung und insbesondere die fachliche Aufteilung eines Systems. Monolithen und Microservices sind „nur“ Implementierungen von Modulen. Wenn die Aufteilung nicht stimmt, ist die Entscheidung für Monolithen oder Microservice nahezu egal. Da dies aber eine technische Entscheidung ist, diskutieren Techniker natürlich sehr gerne. Wichtiger wäre aber das Einhalten fundamentaler Konzepte wie loser Kopplung oder Information Hiding. Die Renaissance von Domain-driven Design und der Fokus auf Monolithen befeuert diese Diskussion, was natürlich positiv ist, denn mit der richtigen Modularisierung kann man viel gewinnen.

TAGS