Monolith vs. Microservices Part 3: Der Richtige Trade-Off für das eigene Projekt
BettercallPaul
BettercallPaul

// //
Lesedauer: 7 Minuten

Monolith vs. Microservices Part 3

Der richtige Trade-off für das eigene Projekt


Im letzten Teil unserer Artikelserie haben wir diskutiert, wie sich die Performance je nach Umsetzungsart unterscheidet. Dabei ist auch aufgefallen, dass sich ein Modulith als Alternative zur Microservice-Architektur eignen kann und dass die Performance bei einer monolithischen Architektur in frühen Projektphasen höher ist als bei einer Microservice-Architektur.

Moni und Michi sind natürlich noch lange nicht am Ende ihrer Diskussion angekommen. Moni favorisiert weiterhin für den Start einen Modulithen, während Michi von Beginn an mit Microservices starten möchte. Aber wie geht es nach dem MVP weiter? Lest einfach selbst:

Zusammenfassung

Architekturarbeit hat oft mit Trade-offs zu tun – und so ist es auch hier. Wenn unabhängige Deployments einen Mehrwert für eine Anwendung bringen und die Vorteile den Aufwand von Abwärtskompatibilität und Tests überwiegen, dann kann man darüber nachdenken, Microservices einzusetzen. Es gibt noch weitere Kriterien für den Einsatz von Microservices – es gibt ja auch noch einige weitere Artikel in dieser Serie.

Wenn der Aufwand, der durch Unterstützung von Abwärtskompatibilität und dem zusätzlichen Aufwand bei automatisierten Tests aber den Aufwand von unabhängigen Deployments überschreitet, dann ist das auch schon ein erstes Indiz dafür, dass eine Microservice-Architektur vielleicht nicht das Richtige für den Anwendungsfall ist. 

Unterschiedliche Geschwindigkeiten

Michi: „Wir müssen langfristig denken und nicht nur an den MVP. Nach dem MVP werden die fachlichen Prozesse in unterschiedlicher Geschwindigkeit ausgebaut. Deshalb sollten die fachlichen Teams ihre Features unabhängig voneinander vorantreiben können.“

Moni: „Da hast du vollkommen Recht. Wie möchtest du das lösen?“

Michi: „Ich würde die Anwendung in ihre fachlichen Prozesse schneiden und für jeden fachlichen Prozess einen Microservice implementieren. Das heißt, dass jedes Team einen fachlichen Prozess betreut und die Verantwortung für diesen Microservice trägt. Die Teams müssen sich nur bezüglich der Schnittstellen abstimmen.“

Moni: „Die Aufteilung der fachlichen Prozesse ist richtig und wichtig, aber aus meiner Sicht erreichen wir das auch mit einem modularen Monolithen. Dadurch kann ich die Entwicklerkapazität über alle fachlichen Prozesse optimal nutzen. Außerdem reduziert sich der Abstimmungsaufwand für Schnittstellen.“

Moni und Michi sind sich einig, dass die Anwendung anhand der fachlichen Prozesse aufgeteilt werden soll. Moni hätte gerne einen Modulith, Michi eine Microservice-Architektur.

Michi begründet die Entscheidung damit, dass die Teams ihre Features und Prozesse unabhängig von anderen Teams entwickeln und deployen können. Beispielsweise kann so der ganze Bestellprozess mit Benutzerinteraktion unabhängig von der Routenoptimierung umgesetzt werden – solange die Schnittstelle zwischen den beiden fachlichen Prozessen abgestimmt ist. Der Bestellprozess bzw. der Microservice dafür kann in der weiteren Entwicklung oft angepasst werden – ganz nach den Rückmeldungen und dem Feedback der Kunden hinsichtlich Usability. Und der Microservice kann so häufig deployed werden, wie das Team möchte. Der Microservice zur Routenoptimierung muss dafür dann nicht angepasst oder neu deployed werden. Diese Architektur unterstützt also unterschiedliche Fortschritte und Geschwindigkeiten bei der Entwicklung der einzelnen fachlichen Prozesse.

Moni entgegnet, dass dies aber auch mit Modulithen erreicht werden kann. Auch dort sind die fachlichen Prozesse in unterschiedlichen Modulen umgesetzt, sodass eine Weiterentwicklung unabhängig voneinander erfolgen kann. Moni ergänzt außerdem, dass sich der Abstimmungsaufwand für Schnittstellen reduziert, da die Schnittstellen der Module im gleichen Repository liegen und Entwickler:innen so jederzeit die Schnittstellen anderer Teams einsehen können. In unserer App könnte also das Team, das den Bestellprozess entwickelt, jederzeit auf den Code des Teams für die Routenoptimierung zugreifen und die Schnittstelle einsehen. Im Zweifel kann das Team des Bestellprozesses die Schnittstelle der Routenoptimierung dann aber auch ändern oder ergänzen.

Abwärtskompatibilität

Moni: „Aus meiner Erfahrung ändern sich auch abgestimmte Schnittstellen während der Entwicklung. In einem modularen Monolith kann so eine Anpassung gegebenenfalls sogar von einer Person durchgeführt werden. Dadurch, dass die Anwendung immer als Ganzes ausgeliefert wird, sind keine inkonsistenten Zustände deployed. Ist das in einer Microservice-Architektur nicht deutlich komplexer?“

Michi: „Es ist komplexer, aber strukturierter. Durch die Abstimmung zwischen den Teams bei Schnittstellen durchlaufen auch Änderungen stets einen Review-Prozess. Schnellschüsse werden dadurch vermieden. Unsere Teams können unabhängig deployen, aber natürlich hast du Recht: Um Inkonsistenzen zu vermeiden, müssen unsere Schnittstellen abwärtskompatibel gestaltet werden.“

Moni führt aus, dass das Thema Abwärtskompatibilität innerhalb eines Modulithen keine Rolle spielt, da die Anwendung immer als Ganzes ausgeliefert wird. Bei diesem Punkt gilt aber Vorsicht! Auch bei monolithischen Systemen kann es sein, dass es unterschiedliche Clients gibt und dass für unterschiedliche Clients unterschiedliche Versionen der Anwendung laufen müssen.

Der zweite Punkt, den Moni anbringt ist, sehr kritisch: Anpassungen an Schnittstellen können modulübergreifend von einer Person erledigt werden. Diese Aussage trifft zwar zu, dennoch ist diese Herangehensweise oft der erste Schritt zum Big Ball of Mud und sollte daher gut überlegt sein. Wenn Teams modulübergreifend Schnittstellen anpassen oder hinzufügen, ist nicht gewährleistet, dass Themen wie Geheimnisprinzip, Datenhoheit oder generell der fachliche Schnitt eingehalten werden. Schnittstellenanpassungen sollten daher auch im Modulith abgesprochen werden und nur nach Rücksprache mit dem betroffenen Team umgesetzt werden. Wenn in unserem Beispiel das Bestellprozess-Team eine Anpassung der Schnittstelle zur Routenoptimierung benötigt, sollte das Bestellprozess-Team diese Anpassung mit dem Team der Routenoptimierung abstimmen und erst nach erfolgter Abstimmung umsetzen oder umsetzen lassen.

Michi entgegnet genau das als Argument. In einer Microservice-Architektur werden solche Schnellschuss-Anpassungen von Schnittstellen vermieden, denn eine Anpassung einer Schnittstelle ist immer ein erheblicher Aufwand. Für eine gewisse Zeit muss ein Microservice zwei Versionen der Schnittstelle bereitstellen – sofern man beim Deployment keine Downtime möchte. Die alte Version muss so lange laufen, bis auch der letzte Client auf die neue Version der Schnittstelle umgestellt hat. Abwärtskompatibilität betrifft oft nicht nur die Schnittstellen (z. B. REST-Schnittstellen oder Messaging), bei denen unterschiedliche Versionen leicht zu unterstützen sind (z.B. andere URL oder Selektor), sondern oft auch die Datenbank. Aber auch auf Datenbank-Ebene können unterschiedliche Versionen unterstützt werden, indem alle Datenbank-Änderungen zweistufig durchgeführt werden. Zunächst werden nur abwärtskompatible Änderungen ausgeführt und sobald die alte Schnittstelle abgeschaltet wird, werden veraltete Spalten oder Views entfernt.

Abwärtskompatible Änderungen sind innerhalb einer Microservice-Architektur täglich Brot und sollten entsprechend früh innerhalb des Projekts geübt und verprobt werden. Durch abwärtskompatible Änderungen entsteht ein Mehraufwand, der aber die Unabhängigkeit der Teams und unabhängige Deployments unterstützt. Aber auch bei einer monolithischen Architektur können abwärtskompatible Änderungen notwendig sein (z. B. auch schon bei Client-Server-Architekturen).

Aufwand beim Testen

Moni: „Die unabhängigen Deployments finde ich gut, allerdings ist mir der Aufwand dafür zu hoch, sodass ich den Ansatz des modularen Monoliths weiter bevorzugen würde.“

Michi: „Ich möchte die Vorteile der unabhängigen Deployments nutzen. Um den Mehraufwand dafür möglichst gering zu halten, ist der richtige fachliche Schnitt entscheidend.“

Ein weiteres Thema, das für unabhängige Deployments und unabhängige Entwicklung wichtig ist, sind Aufwände für Tests.

Bei Modulithen sind automatisierte Tests im Vergleich zu einer Microservice-Architektur leicht umsetzbar. Dies ist schlicht dadurch bedingt, dass das ganze System als eine Einheit deployed wird und auch als solche getestet werden kann. Man kann beim Test selbst entscheiden, ob man ein Modul mocked oder die tatsächliche Implementierung eines Moduls benutzt – das ist straight-forward.

Bei einer Microservice-Architektur ist das schon deutlich komplexer. Ein Aufruf eines anderen Moduls bedeutet immer ein Aufruf über das Netzwerk, ggf. noch über dedizierte Middleware wie Kafka. Auch bei einer Microservice-Architektur kann man das Verhalten eines einzelnen Microservice relativ leicht testen. Aber wie kann das korrekte Zusammenspiel von Microservices sichergestellt und automatisiert getestet werden? Muss man dafür extra den ganzen Zoo aus unterschiedlichen Microservices auf einer dedizierten Umgebung starten, um einzelne Anwendungsfälle durchzuspielen? Nein – natürlich nicht. Dafür gibt es das Konzept von Contract-Driven-Tests (z. B. mit Pact –  https://docs.pact.io/). Wie funktioniert das?

  1. Consumer-Microservice testet sein Verhalten gegen Mocks.
  2. Erwartetes Verhalten des Mocks und benötigte Aufrufe werden in einen Contract geschrieben.
  3. Der Contract wird über Teams hinweg geteilt (z. B. über Pactflow).
  4. Der Contract wird gegen die angebotenen Schnittstellen des Provider-Microservices gehalten und Tests gegen die angebotenen Schnittstellen ausgeführt. Die Ergebnisse werden mit den erwarteten Ergebnissen des Consumers abgeglichen.
  5. Beim Test des Providers werden weitere Schnittstellenaufrufe gemocked, sodass dieser in Isolation getestet werden kann.

Hinter diesem Vorgehen steckt einiges an Aufwand, jedoch können die einzelnen Microservices so isoliert voneinander getestet werden und damit auch unabhängig voneinander deployed werden. Damit dieser Mehraufwand möglichst gering bleibt, ist es wichtig, dass Microservices möglichst wenig Schnittstellen aufrufen. Viele Schnittstellenaufrufe deuten auf einen schlechten fachlichen Schnitt hin.