Über unsMediaKontaktImpressum
André Kappes 11. Juli 2023

Modulare Monolithe statt Microservices

Keep it simple, stupid!

Monolithe werden als Architekturmuster verkannt. Zu oft verwandeln sie sich in einen "Big Ball of Mud": Alles hängt mit allem zusammen: Die gewollte Änderung einer Code-Stelle zieht zehn absolut ungewollte irgendwo anders nach sich. Das ist alles korrekt. Wenn wir diese Koppelung aber in den Griff bekommen, sind Monolithe vor allem eins: einfach.

Es lohnt sich deshalb, dieses Muster genauer in Augenschein zu nehmen: Welche Vorteile bietet es gegenüber Microservices? Wie macht man die Nachteile durch Modularisierung beherrschbar? Machen wir uns also auf den Weg zu "modularen Monolithen". Bei dieser Reise werden wir immer wieder feststellen: Modulare Monolithe sind nicht nur ein Gegenentwurf zu Microservices, sie sind auch mit ihnen verwandt.

Monolith = Deployment-Monolith

Unter "Monolith" – oder genauer "Deployment-Monolith" – verstehen wir ein System, das nur aus einem Deployment-Artefakt besteht. Wir ändern also zum Beispiel nur eine .jar-Datei, wenn wir eine neue Version der Anwendung ausrollen. Microservices oder Self-Contained Systems sind dagegen keine Deployment-Monolithen: Sie bestehen aus mehreren Deployment-Artefakten – hier kann man die Teile des Systems getrennt voneinander ausrollen. Ausdrücklich erlaubt ist bei Monolithen, dass man horizontal skaliert, also mehrere Instanzen mit demselben Artefakt betreibt.

Monolithen sind einfach

Einfachheit ist der große Vorteil von Monolithen: Die Infrastruktur, das Deployment, die Versionierung, das Refactoring sind viel einfacher im Vergleich zu Microservices. Wir brauchen keine Mechanismen, um alle Zeilen im Log zusammenzuführen, die von einem Request ausgelöst worden sind, denn diese sind nicht über mehrere Services verstreut. Um eine neue Version auszurollen, müssen wir idealerweise nur auf einen Knopf drücken. Wir haben nur ein Source-Code-Repository und – im Fall von Webservices – eine Version, anstelle eines Repositorys und einer Version für jeden unserer Microservices. Damit müssen uns nicht darum kümmern, ob Microservice A in Version 1.2.25 mit Microservice B in Version 2.1.8 zusammenarbeiten kann. In unserem Source-Code-Repository können wir außerdem leicht Code von A nach B schieben.

Ein Monolith ist kein verteiltes System, deshalb hat er auch nicht die Probleme, die verteilte Systeme haben. Wenn zwei Services miteinander kommunizieren, wissen wir im Fehlerfall nicht, ob Requests beim Ziel angekommen sind oder ob wir einfach länger warten müssten – im Monolith gibt es keine Netzwerk-Kommunikation innerhalb der Anwendungsschicht. Sobald eine Geschäftstransaktion auf mehrere Microservices aufgeteilt ist, wird es schnell sehr aufwändig, weil wir Muster wie "Two Phase Commit" oder ein "Saga"-Pattern einsetzen müssen [1]. Im Monolith dagegen läuft alles in einem Prozess und wir haben die Transaktionsgrenzen einfach unter Kontrolle.

Modularisierung to the rescue

Leider ist es bei Monolithen sehr einfach, Code zu koppeln, der nicht gekoppelt sein sollte – daher auch ihr schlechter Ruf als "Big Ball of Mud". Die Lösung für einfache Koppelungen kennen wir schon seit den 70ern: wir modularisieren. Das heißt, wir zerlegen unser System in abgegrenzte Bausteine mit hoher Kohäsion und loser Koppelung und klaren Schnittstellen zwischen den Bausteinen. Wir pimpen unsere Monolithe zu modularen Monolithen (auch "Modulithe" genannt) – und erhalten so die Lösung für alle unsere Probleme.

Natürlich sind modulare Monolithe nicht die allumfassende Lösung, denn wir kommen nicht in den Genuss der Vorteile von Microservices. Der Build-Prozess ist langwieriger. Alles läuft in einer Laufzeit-Umgebung. Damit kann man zur Lastverteilung und Ausfallsicherheit nur insgesamt skalieren, anstatt einzelne Teile gezielt zu skalieren. Ein Fehler in einem Teil der Anwendung kann unter Umständen das gesamte System lahmlegen. Wenn man eine neue Version ausrollt, muss man immer alles komplett ausrollen. Damit wird es schwieriger, mit mehreren Teams parallel zu entwickeln, weil alle sich im gemeinsamen Deployment treffen müssen und nicht unabhängig liefern können.

Am Ende ist es eine Architektur-Entscheidung und wir müssen abwägen: Gibt es gute Gründe, die gegen einen Monolithen sprechen? Verstehen wir die fachlichen Anforderungen so gut, dass wir uns einigermaßen sicher sind hinsichtlich der Aufteilung des Systems in Bausteine? Oder wollen wir im Sinne von "Keep it simple, stupid!" mit etwas Einfachem anfangen und den Monolithen als Sprungbrett für eine spätere Aufteilung in Microservices nutzen?

Wie vorgehen?

Angenommen, wir entscheiden uns, eine monolithische Architektur umzusetzen. Damit wir die Koppelung im Griff behalten und unser System wartbar bleibt, setzen wir auf Modularisierung. Also müssen wir uns als erstes über die Module Gedanken machen, in die wir unsere Applikation aufteilen. Wenn wir gut modularisieren, werden die Module wenig untereinander kommunizieren müssen. Dennoch werden sie teilweise miteinander interagieren; dafür müssen wir einen Weg vorsehen. Gleichzeitig möchten wir nicht riskieren, versehentlich Verbindungen zwischen den Modulen einzubauen; daher sollten wir idealerweise darauf gestoßen werden, wenn wir die bestehende Modularisierung durch neue Features verletzen. Diese drei Fragen müssen wir also beantworten:

  • Wie erhalten wir Module mit hoher Kohäsion und loser Koppelung?
  • Wie sorgen wir dafür, dass die Module getrennt bleiben?
  • Wie gestalten wir die Kommunikation zwischen den Modulen?

Module mit hoher Kohäsion und loser Koppelung

Fachlich vor technisch

Module sind klar erkennbare Bausteine, die sich jeweils um getrennte Belange kümmern (Separation of Concerns) und durch bewusst entworfene Schnittstellen miteinander kommunizieren. Die erste Frage ist: Schneiden wir die Module fachlich oder technisch? Technische Schnitte, z. B. in Datenbank-Schicht, Business-Schicht und REST-API-Schicht (s. Listing 1), sind naheliegend, aber sie verletzen das Single-Responsibility-Prinzip. Für jeden neuen oder geänderten Use Case müssen wir jede dieser Schichten anfassen. Damit gibt es in jedem technischen Modul viele Gründe für Veränderungen. Fachliche Modulschnitte sind in der Regel besser, denn fachliche Erweiterungen und Änderungen betreffen einen Use Case oder eine Gruppe von Use Cases, während die anderen unbeeinflusst bleiben. Aus dem gleichen Grund hat ein technischer Schnitt eine niedrige Kohäsion und eine starke Koppelung zwischen den Modulen: Für jeden Use Case müssen die Module miteinander kommunizieren, aber innerhalb der gleichen Schicht haben die verschiedenen Use Cases selten etwas miteinander zu tun. Fachliche Module verfügen dagegen über eine hohe Kohäsion und lose Koppelung, weil es viele fachliche Gründe für Kommunikation innerhalb eines Moduls gibt und wenige dafür, dass Module untereinander reden.

Listing 1: Technische Modularisierung.

src/main/java
 ├── rest
 ├── domain
 └── persistence

Fachliche Module finden

Das erste Problem besteht also darin, die fachlichen Anforderungen sinnvoll zu zerlegen. Diese Herausforderung bekommen wir frei Haus, unabhängig davon, ob wir modulare Monolithen oder Microservices implementieren. Um sie zu meistern, helfen Techniken aus dem Domain-driven Design wie Event Storming oder Domain Story Telling[2]. Mit ihnen finden wir auf jeden Fall einen guten initialen Schnitt der Module. Nun werden wir im Laufe der Zeit bestimmt fachliche Aspekte dazulernen. Das ist für sich genommen erfreulich, bedeutet aber, dass wir den anfänglichen Entwurf immer wieder anpassen müssen. Den perfekten fachlichen Schnitt ohne Verfallsdatum gibt es wohl nicht.

Module sichtbar machen

Was sieht ein neues Teammitglied, wenn es zum ersten Mal den Code öffnet? Im Idealfall "schreit" die Architektur aus dem Code heraus, wie es Robert Martin fordert [3]. Die Module sind klar zu erkennen und man kann sofort ahnen, was die Applikation alles macht. Wenn wir also ein Zeiterfassungssystem implementieren, sollte uns die Ordnerstruktur direkt verraten, dass es hier darum geht, Zeiten zu erfassen, Urlaub zu buchen und Krankmeldungen abzugeben (Listing 2).

Listing 2: Fachliche Modularisierung

src/main/java
 ├── application
 ├── krankheit
 ├── urlaub
 ├── zeiterfassung
 └── infrastructure

Belegtes Sandwich

Bei dem Versuch, die Modularisierung zu implementieren, stellt man fest, dass man um die fachlichen Module herum gegebenenfalls einen Container benötigt, der sie alle kennt und bündelt. In unserem Beispiel des Zeiterfassungssystems leistet dies das Modul application. Bei Microservices wird diese Aufgabe von der sie umgebenden Infrastruktur erfüllt: Sie sorgt dafür, dass die einzelnen Services laufen und sowohl von außen als auch untereinander erreichbar sind.

In den einzelnen fachlichen Modulen benötigt man außerdem Wege, um eine Datenbankverbindung aufzubauen, um E-Mails zu verschicken, um Audit-Logs oder Ähnliches zu führen, kurz: für den ganzen technischen Kram, den man aus den fachlichen Modulen heraushalten will. Das infrastructure-Modul in unserem Beispiel nimmt diese technischen Belange auf und darf von allen Modulen verwendet werden. Bei Microservices wird man dafür das Sidecar-Pattern oder eine gemeinsam genutzte Bibliothek verwenden, die von allen Services eingebunden wird – zumindest, wenn man "Shared Nothing Architecture" nicht wortwörtlich nimmt.

Das Sandwich aus application oben und infrastructure unten ist also belegt mit den fachlichen Modulen. Man muss aber aufpassen, dass in diesen beiden Modulen keine Fachlogik steckt, denn das soll sie nicht. Es ist verführerisch, hier Code hineinzupacken, der alle anderen fachlichen Module braucht oder der von mehreren fachlichen Modulen gebraucht wird.

Module getrennt halten – Dem Big Ball of Mud entgegenwirken

Wer kennt wen?

Zyklische Abhängigkeiten sind ein Problem, zunächst für unser Gehirn, denn Rückbezüge können wir mit ihm schlecht erfassen. Hierarchische Abhängigkeiten sind viel einfacher für uns, aber auch für Computer. Module, die Teil eines Zyklus sind, sind de facto nicht trennbar; Microservices, die im Kreis miteinander kommunizieren, verbrauchen im besten Fall nur mehr Strom, im Normalfall kollabiert die gesamte Anwendung.

Wir müssen also zyklische Abhängigkeiten verhindern. Nicht nur das – insgesamt möchten wir die Abhängigkeiten zwischen den Modulen möglichst gering halten. Wenn wir eine neue Abhängigkeit einführen, sollten wir eine Warnung bekommen, damit wir uns mit einigen wichtigen Fragen auseinandersetzen. Beispielsweise damit, ob die Abhängigkeit in die richtige Richtung läuft oder ob wir sie mittels Dependency Inversion umkehren müssen. Oder ob die Abhängigkeit aufgrund von Feature-Neid entsteht. Oder ob wir dabei sind, durch die neue Abhängigkeit einen Zyklus einzubauen. Wir brauchen "Brandmauern" zwischen unseren Modulen, die nicht einfach zu durchbrechen sind. Bei Microservices wird dies ein Stück weit erfüllt von dem Overhead, der mit einer Kommunikation zwischen den Services einhergeht. Bei Monolithen müssen wir diese Brandmauern bewusst einziehen. Dafür haben wir eine Reihe von Möglichkeiten.

ArchUnit

Mit ArchUnit kann man Architektur-Tests schreiben, die fehlschlagen, wenn neue Abhängigkeiten zwischen unseren Modulen eingeführt werden [4]. Als Beispiel möchten wir Zugriffe von urlaub auf krankheit verbieten (s. Listing 3).

Listing 3: ArchUnit-Test, der den Zugriff von urlaub auf krankheit verbietet

public void urlaubDoesNotAccessKrankheit {
    noClasses().that()
            .resideInAPackage("de.andrena.star.urlaub..")
            .should()
            .dependOnClassesThat()
            .resideInAPackage("de.andrena.star.krankheit..")
            .check(classes);
}

Wenn wir versehentlich oder absichtlich eine Abhängigkeit von urlaub nach krankheit einbauen, schlägt der Test fehl und wir werden gezwungen, uns darum zu kümmern. ArchUnit ist leichtgewichtig: Wir können ohne weiteres direkt Regeln einführen und peu à peu verschärfen, was sich anbietet, wenn wir schon ein Stück weit in Richtung Big Ball of Mud unterwegs waren. Dank der Fluent-API sind die Regeln für neue Teammitglieder leicht verständlich. Allerdings erkaufen wir uns diese Vorteile durch eine schwache technische Trennung. Sollten wir zum Beispiel nicht der erwarteten Namenskonvention von Packages folgen, bleiben die Tests grün. Außerdem findet die Überprüfung erst beim Ausführen der ArchUnit-Tests statt und nicht schon beim Kompilieren des Codes. Dennoch ist dieses Prinzip so gut, dass in "Spring Modulith" ein Spring-Projekt entwickelt wird, mit dessen Hilfe man Architektur-Regeln formulieren kann, die intern mit ArchUnit überprüft werden [5].

Multi Module Projects mit Maven

Mit einer Überprüfung zur Compile-Zeit bekommen wir früher die Rückmeldung, dass der Code, den wir schreiben wollen, unsere Architektur-Regeln verletzt. Wir können z. B. Maven dafür einsetzen und unser Maven-Projekt in mehrere Modules aufteilen. Innerhalb eines Modules ist es dann nur möglich, die anderen Modules zu verwenden, die in der pom.xml als Dependencies aufgeführt sind. Wenn man eine neue Abhängigkeit zwischen den Modulen einführt, muss man damit bewusst das pom-File anpassen. Zyklen zwischen den Modulen sind automatisch verboten, da Maven das Gesamtprojekt sonst nicht bauen kann.

Weitere Modularisierungstechnologien

Man kann auch Gradle anstelle von Maven verwenden. Dann hat man zusätzlich den Vorteil, dass Gradle beim inkrementellen und parallelen Bauen intelligenter ist als Maven und sich dadurch gegebenenfalls die Build-Zeiten verkürzen.

Mit dem Java-9-Module-System (Jigsaw) kann man zusätzlich die öffentliche Schnittstelle eines Moduls verkleinern, indem man bewusst nur das zugreifbar macht, was in der öffentlichen Schnittstelle sein soll [6].

Kommunikation zwischen den Modulen

Module werden miteinander reden müssen, denn sonst wären sie besser nicht Bestandteile eines größeren Systems. Deshalb ist es unerlässlich, einen Weg zur Kommunikation vorzusehen.

Wie so oft gibt es dafür etablierte Muster, die wir für unseren Einsatzzweck anpassen können: Messaging, Direct Call und Shared Database Data [7]. Schauen wir uns also die drei Muster genauer an und wählen dazu zwei Anwendungsfälle aus unserem Fallbeispiel, die Kommunikation erfordern:

Zum Beispiel möchte man in der Zeiterfassung sehen, ob man an einem bestimmten Tag Urlaub gebucht oder sich krankgemeldet hat. Die Zeiterfassung braucht also eine Sicht auf die Urlaubs- und Krankheitsdaten, mindestens auf der UI-Ebene. Allerdings reicht für diesen Anwendungsfall eine eingeschränkte Sicht auf die Daten. Wichtig ist, dass für den betreffenden Tag eine Krankmeldung vorliegt, ob z. B. auch eine Arbeitsunfähigkeitsbescheinigung abgegeben wurde, ist nicht wichtig.

Der zweite Anwendungsfall betrifft eine Krankmeldung während eines Urlaubs. Diese soll dazu führen, dass der zuvor gebuchte Urlaub während dieses Zeitraums storniert wird. In diesem Fall ist also Wiederverwendung von Business-Logik der Grund für die Kommunikation.

Messaging

Messaging hilft, Microservices möglichst lose zu koppeln. Es gibt eine zentrale Stelle, den Message Bus, bei dem man Nachrichten eines bestimmten Typs abonnieren kann und dann alle Messages dieses Typs erhält. Bei modularen Monolithen befindet sich der Message Bus im Code statt in der Infrastruktur und ist allen Modulen bekannt.

Jedes Modul hat dabei selbst die Hoheit über seine Daten, was gleichzeitig heißt, dass es für die Daten anderer Module weder Lese- noch Schreibrechte besitzt. Wenn es Daten anderer Module braucht, muss es diese bei sich repliziert speichern. Erlaubt ist durchaus, innerhalb des modularen Monolithen den gleichen Datenbank-Server zu verwenden, nur werden die Tabellen nicht zwischen den Modulen geteilt, stattdessen ist jedes Modul Herr über eine Menge an Tabellen.

Das urlaub-Modul schickt z. B. im Fall einer neuen Urlaubsbuchung eine Nachricht; das zeiterfassung-Modul hört auf diese Nachrichten und merkt sich, dass an einem bestimmten Tag Urlaub gebucht wurde. Genauso hört das urlaub-Modul auf Krankmeldungs-Nachrichten und storniert gegebenenfalls den Urlaub (s. Abb. 1).

Der größte Vorteil von Messaging ist die lose Koppelung: Module kennen sich nicht gegenseitig, sondern nur den Message Bus und das Datenaustausch-Format. Die Business-Logik, z. B. für die Stornierung von Urlaub, befindet sich außerdem an einer einzigen Stelle und die getrennte Datenhaltung macht es einfach, die Module später in einzelne Services auseinanderzuziehen.

Die getrennte Datenhaltung und die damit verbundene Daten-Replikation bringen jedoch auch einiges an Komplexität mit sich. Immerhin muss man sich weniger Sorgen um die Konsistenz der Daten machen als bei Microservices, weil keine Netzwerkkommunikation notwendig ist. Deshalb kann man sich sicher sein, dass die Daten angekommen sind. Ein weiterer Nachteil: Durch die Indirektion über Messages statt des Methoden-Aufrufs ist der Datenfluss außerdem etwas schwerer nachzuvollziehen.

Direct Call

Letzteres ist etwas einfacherer, wenn die Module mittels direkter Aufrufe miteinander reden. Bei Microservices setzt man für dieses Muster typischerweise auf eine Kommunikation über REST-APIs; bei modularen Monolithen kann man diese Kommunikation einfacher realisieren, weil man nur Methoden-Aufrufe benötigt. Folglich entfällt der gesamte Overhead der Netzwerkkommunikation und der damit verbundenen Serialisierung, De-Serialisierung, Authentifizierung und Autorisierung.

Kommunikation per Direct Call führt allerdings zu einer stärkeren Koppelung, denn der Aufrufer kennt das Interface des Aufgerufenen. Außerdem muss man – wie bei Microservices auch – auf die Vermeidung zyklischer Abhängigkeiten achten.

Die Datenhoheit liegt weiterhin bei den einzelnen Modulen. Wenn also ein Urlaub gebucht wird, sagt das urlaub-Modul dem zeiterfassung-Modul per Methoden-Aufruf Bescheid. Dann merkt sich das zeiterfassung-Modul, dass an bestimmten Tagen Urlaub gebucht ist. Wenn eine Krankmeldung erfasst wird, ruft die krankmeldung das urlaub-Modul auf, damit es den Urlaub während des Krankheitszeitraums storniert (s. Abb. 2).

Wie sehen die Parameter dieser Methoden-Aufrufe aus? Beim Stornieren des Urlaubs sollen in unserem Beispiel drei Dinge übermittelt werden: der Zeitraum, der betroffene Mitarbeiter und der durchführende Benutzer. Möchten wir generische Typen dafür verwenden oder eigene Typen einsetzen? Eigene Typen sollten die Rolle von DTOs (Datentransfer-Objekten) haben, denn damit kann man die interne Repräsentation der Daten eines Moduls von der Schnittstelle trennen. Der Preis dafür ist der Mapping-Overhead zur Übersetzung von Schnittstellen-Typ in einen internen Typ. Solange die Parameterliste also noch klein genug ist, kann man mit primitiven Typen oder JDK-eigenen Typen auskommen und spart sich diesen Overhead.

Shared Database Data

In den bisherigen Kommunikationsmustern mussten wir Daten replizieren, die zwischen Modulen geteilt werden. Bei Shared Database Data teilen wir die Datenbank-Schicht zwischen den Modulen. Das urlaub-Modul speichert die Urlaubsbuchung in der Datenbank und die zeiterfassung greift über den gleichen Weg lesend auf die Urlaubsbuchung zu. Bei einer Krankmeldung storniert das krankheit-Modul die Urlaubsbuchung für den Zeitraum, indem es ebenfalls direkt auf der Datenbankschicht operiert (s. Abb. 3).

Der Vorteil besteht darin, dass man keine Datenreplikation zwischen den Modulen benötigt. Im Beispiel sieht man aber deutlich, wie teuer dieser Vorteil erkauft wird. Die Module sind über die Datenbank stark miteinander gekoppelt und es kann beispielsweise im Fall der Urlaubsstornierung nötig werden, Business-Logik in mehreren Modulen, urlaub und krankheit, zu implementieren.

Wir müssen den Shared-Database-Data-Ansatz also zähmen, wenn wir das Muster sinnvoll einsetzen wollen. Die Duplikation von Business-Logik sollten wir in jedem Fall vermeiden; für diesen Fall brauchen wir eines der anderen Kommunikationsmuster. Auf Datenbankseite sollte jedes Modul die schreibende Hoheit über bestimmte Tabellen haben. Andere Module dürfen darauf höchstens lesend zugreifen.

Als zweite Maßnahme können wir z. B. Spring Data-Projections [8] oder ein explizites Mapping von Datenbank-Entities zu Fachobjekten nutzen, um die Datenbankrepräsentation von der Verwendung innerhalb eines Moduls zu entkoppeln. Für die zeiterfassung ist nur wichtig, dass an einem Tag Urlaub gebucht wurde. Wer ihn wann gebucht hat, interessiert dieses Modul nicht, und es kommt mit einer abgespeckten Sicht auf die Daten blendend zurecht.

Mit diesen beiden Gegenmitteln wird Shared Database Data als Muster erträglich. Wenn man für später eine Aufspaltung in mehrere Services mit getrennter Datenhaltung anstrebt, ist die Verwendung allerdings eine Hürde, die man zuvor aus dem Weg räumen muss.

Die Kommunikationsmuster in der Praxis

Alles klar, wir wählen also Muster 1, 2 oder 3. Doch wie wir gerade gesehen haben, fängt die Arbeit damit erst an: Wie genau wollen wir die Muster einsetzen? Und wie erhalten wir darüber im Entwicklungsteam ein gemeinsames Verständnis? Das Problem ist allgemein – wir werden es bei Microservices genauso antreffen.

Ein Ansatz zur Lösung ist: Wir geben uns gemeinsam Regeln für die Kommunikation zwischen den Modulen. Diese müssen möglichst konkret sein, damit ich als Entwickler weiß, was ich tun soll, und möglichst einfach, damit ich sie nachvollziehen kann. Die Regeln dürfen auch nicht in Stein gemeißelt sein, denn wir werden immer wieder dazulernen oder in Probleme laufen, die eine Anpassung der Regeln notwendig machen – Inspect and Adapt eben.

Beispiele für eine Regel:

  • Fachliche Module dürfen sich, wenn notwendig, auch gegenseitig aufrufen
  • Zur Wiederverwendung von Business-Logik einsetzen
  • Als Aufruf-Parameter keine DTOs verwenden, sondern primitive Typen oder Common-Typen wie "Zeitraum", um den Mapping-Overhead zu vermeiden
  • Nicht zum bloßen Holen von Daten einsetzen

Inspect and Adapt

Durch einen guten Modulschnitt, durch Brandmauern zwischen den Modulen und durch klare Kommunikationsmuster können wir unseren Monolithen also wartbar halten. Reicht es, diese drei Aspekte ein für alle Mal zu beantworten? Wohl kaum: Wir werden immer wieder über fachliche Anforderungen stolpern, die nicht in unsere bisherigen Modulschnitte passen. Deshalb werden wir den Modulschnitt immer wieder überdenken müssen. Wir werden die Brandmauern zwischen den Modulen und die Regeln für die Kommunikation der Module immer wieder anpassen müssen, weil wir mehr über die Fachdomäne gelernt haben oder uns mit der Zeit technischer Sünden bewusst werden, die wir am Anfang begangen haben, ohne es zu wollen. Um Inspect and Adapt kommen wir also nicht herum. Ein modularer Monolith unterstützt uns dabei, weil Refactoring einfach ist. Wenn wir also keine guten Gründe für einen starken technischen Split in Microservices haben, können wir die größere akzidentielle Komplexität, den Overhead, der mit Microservices einhergeht, vermeiden und es uns mit modularen Monolithen einfacher machen – im Sinne von "Keep it simple, stupid!".

Autor

André Kappes

André Kappes arbeitet seit 2015 bei der andrena objects ag und ist Software Craftsman aus Leidenschaft. Er ist konstant auf der Suche nach Mitteln und Wegen, um handwerklich gute Software zu entwickeln.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben