adesso Blog

Drei Situationen, ein Muster

In Architekturdiskussionen begegnen mir immer wieder dieselben drei Situationen.

Ein Projekt startet neu. Microservices stehen fest, bevor die erste fachliche Frage geklärt ist. Alternativen werden nicht geprüft, weil sich die Frage nie stellt.

Ein gewachsenes System gilt als gescheitert. Weil der Monolith das Problem sein muss, steuert das Team ins andere Extrem und will alles verteilen.

Eine neue Anforderung passt in keinen bestehenden Zuschnitt. Niemand weiß so recht, wohin damit, also entsteht ein neuer Service.

So unterschiedlich die Ausgangslagen sind, sie enden an derselben Stelle: Verteilung war keine Entscheidung, sondern der Default.

Mein Kollege Christian Ortiz hat in Spring Modulith als Alternative zu Microservices gezeigt, wie sich ein System sauber modularisieren lässt. Mir geht es um die Frage davor: Wann ist Verteilung überhaupt das richtige Mittel?

Ein gescheiterter Monolith beweist nichts über Verteilung

Der zweite Fall enthält den verbreitetsten Fehlschluss. Ein System ist über Jahre unwartbar geworden: Jede Änderung erzeugt Seiteneffekte, niemand weiß genau, welchem Team welcher Code gehört, fachliche Grenzen sind nicht mehr erkennbar. Daraus wird geschlossen, eine Anwendung sei das Problem und viele Anwendungen die Lösung.

Dieser Schluss hält nicht. Was ihr dort erlebt, ist kein Monolith, sondern ein Big Ball of Mud. Der entsteht nicht dadurch, dass alles in einem Artefakt läuft, sondern dadurch, dass fachliche Grenzen fehlen, die Kopplung zu hoch ist und Verantwortlichkeiten unklar sind. Nichts davon hat mit der Anzahl eurer Deployments zu tun.

Ein unwartbares System ist deshalb kein Argument gegen ein Deployment, sondern ein Beleg für fehlende Grenzen. Wer sie mit Verteilung beantwortet, nimmt das Problem mit und stellt ein Netzwerk dazwischen.

Die Grenzen kennt ihr am Anfang noch nicht

Häufig läuft die Begründung über Domain-Driven Design: Eine Anforderung passt in keinen bestehenden Kontext, also ist es ein neuer Bounded Context, daher braucht es einen neuen Service.

Der erste Teil kann stimmen. Die Schlussfolgerung kann aber falsch sein. Ein Bounded Context beschreibt, wo ein Fachmodell gilt und wo ein anderes beginnt. Er ist ein Modellierungsbegriff und sagt nichts darüber, in wie vielen Prozessen euer System läuft. Ein Kontext kann ein Modul sein, eine Bibliothek oder ein Service.

Dazu kommt ein zweites Problem: Zu Beginn ist der richtige Schnitt meistens noch gar nicht bekannt. Fachlichkeit erschließt sich im Bauen, Annahmen erweisen sich als falsch, Zuständigkeiten verschieben sich. Das ist normal und kein Planungsfehler.

Entscheidend ist, was eine Korrektur kostet. Verläuft die Grenze innerhalb eines Deployments, ist ihre Verschiebung ein Refactoring: Code wandert, Schnittstellen ändern sich, die Tests laufen weiter. Verläuft sie zwischen Services, wird daraus ein eigenes Vorhaben: Daten migrieren, Verträge neu verhandeln, Deployments koordinieren.

Genau deshalb ist Verteilung als Default riskant: Sie zementiert einen Zuschnitt zu dem Zeitpunkt, an dem ihr am wenigsten über die Fachlichkeit wisst.

Die Rechnung kommt später

Das Muster ist auch deshalb stabil, weil ein neuer Service zunächst funktioniert: klein, überschaubar, kaum Abhängigkeiten. Die Entscheidung sieht gut aus.

Teuer wird sie bei der ersten Abhängigkeit zu einem anderen Kontext. Dann stehen Fragen im Raum, die vorher niemand gestellt hat. Synchroner Aufruf oder Event? Was passiert, wenn der andere Service nicht erreichbar ist? Wie rollen wir eine fachliche Operation zurück, die über zwei Services läuft?

Schlimmer ist der Fall, in dem diese Fragen gar nicht erst auftauchen. Dann wird der HTTP-Call eingebaut, weil er funktioniert. Oder es gehen Events über die vorhandene Messaging-Infrastruktur, weil sie ohnehin da ist. Monate später schlagen Events fehl, ohne dass es jemand bemerkt, weil nie definiert wurde, wem der Fehlerfall gehört.

Die Kosten der Verteilung entstehen also nicht beim Schnitt, sondern bei der ersten Kopplung. Und da ist die Struktur längst festgelegt.

Der Distributed Big Ball of Mud

Sind die Grenzen nicht geklärt, verschwindet die Kopplung durch Verteilung nicht, sie wandert über Netzwerkgrenzen. Dazu kommen Latenz, verteilte Transaktionen und eine Betriebslandschaft, die selbst gepflegt werden will.

Segment hat 2018 in Goodbye Microservices beschrieben, wohin das führt. Damit eine ausgefallene Integration nicht alle anderen ausbremst, bekam jede Ziel-Integration einen eigenen Service mit eigener Queue. Die Isolation funktionierte, die Kopplung blieb: Alle Services hingen an denselben gemeinsamen Bibliotheken, deren Versionen mit der Zeit auseinanderliefen. Bei über 140 Services waren drei Leute in Vollzeit damit beschäftigt, das System am Laufen zu halten. Nach der Rückführung in einen Monolithen war das Problem weg. Nicht, weil Services falsch sind, sondern weil der Schnitt es war.

Wann Verteilung sich lohnt: fünf Kriterien

Microservices sind eine valide Architektur und vor allem ein Betriebsmodell: Observability, verteiltes Tracing, Datenkonsistenz über Servicegrenzen, komplexere Pipelines, Sicherheit pro Service, Bereitschaftsdienste. Das ist kein Beiwerk, sondern die halbe Miete. Die Investition lohnt sich, wenn mindestens eines dieser Kriterien belastbar zutrifft:

  • Harte Isolationsanforderungen, etwa Mandantentrennung oder regulatorische Auflagen, die getrennte Laufzeiten verlangen
  • Stark abweichende Lastprofile oder Verfügbarkeitsziele einzelner Komponenten
  • Extern getaktete Release-Zyklen, vorgegeben durch Partner oder regulatorische Termine
  • Technologische Notwendigkeit, etwa ein Modul mit zwingend anderem Technologiestack
  • Mehrere Teams mit eigenem Release-Takt, wenn die Koordination am gemeinsamen Artefakt teurer wird als verteilter Betrieb

Wer keines dieser Kriterien bejahen kann, hat kein Verteilungsproblem.

Und wer eines bejahen kann, hat es für ein Modul, nicht für das System. Die Kriterien gelten je Kontext: Trifft eines auf das Reporting zu, wird das Reporting extrahiert. Der Rest bleibt, wo er ist. „Wir haben abweichende Lastprofile, also Microservices" ist derselbe Fehlschluss wie „unser Monolith ist unwartbar, also Microservices", nur mit besserer Begründung.

Die bessere Standardantwort

Wenn Verteilung eine bewusste Entscheidung sein soll, braucht es einen Default, der ohne sie auskommt. Das ist der Modulith: ein Deployment, aber klar geschnittene Module mit definierten Schnittstellen, eindeutiger Verantwortung und kontrollierten Abhängigkeiten. Die Disziplin guter Microservices, angewendet innerhalb eines Prozesses.

Der Vorteil im Alltag ist konkret. Ein fehlgeschlagener Aufruf ist ein Stacktrace, kein Trace über vier Systeme. Ein Event verschwindet nicht unbemerkt in einer Queue, für die sich niemand zuständig fühlt. Und eine Grenze, die sich als falsch erweist, lässt sich verschieben.

Ein Einwand, der an dieser Stelle regelmäßig kommt: „Wir wollen aber ereignisgetrieben arbeiten." Das ist kein Argument für Verteilung. Events beschreiben, wie Module miteinander sprechen, nicht in wie vielen Prozessen sie laufen. Innerhalb eines Deployments sind sie sogar einfacher, weil Zustellung und Transaktion zusammenfallen. Wie sich Fachlichkeit über Events modellieren lässt, habe ich in DDD, CQRS, ES und die Rolle von Axon beschrieben.

Der eigentliche Test: die Daten

Die meisten Modulith-Diskussionen drehen sich um Paketstrukturen. Dort scheitert eine spätere Extraktion aber nicht. Die Datenbank ist es.

Ein Architekturtest prüft, ob Modul A eine Klasse aus Modul B importiert. Er merkt nicht, wenn das Repository von Modul A per Join auf dessen Tabellen zugreift oder eine Transaktion Daten in drei Modulen gleichzeitig ändert.

Schneidet die Daten deshalb so konsequent wie den Code:

  • Ein Schema pro Modul. Mit eigenem Datenbankbenutzer scheitert ein fremder Zugriff nicht erst im Review, sondern im Integrationstest.
  • Keine modulübergreifenden Joins und keine Fremdschlüssel über Modulgrenzen. Referenzen auf fremde Aggregate sind IDs.
  • Die Transaktionsgrenze ist die Modulgrenze. Was heute bequem in einer Transaktion läuft, ist nach der Extraktion ein verteiltes Konsistenzproblem.
  • Migrationen pro Modul, damit es samt Historie lösbar bleibt.

Braucht ein Modul fremde Daten, gibt es zwei Wege: über die öffentliche Schnittstelle des anderen Moduls oder über ein eigenes Read Model, das per Event gefüllt wird. Beides funktioniert nach einer Extraktion unverändert weiter, ein Join nicht.

Ein Modulith ohne diese Regeln ist kein Sprungbrett, sondern ein sauber sortierter Big Ball of Mud: aufgeräumt im Code, verwoben in den Daten. Sind die Grenzen dagegen auch im Schema sauber, bleibt für eine spätere Extraktion nur der Austausch des Transports: kalkulierbare Arbeit, die ein Modul betrifft statt des ganzen Systems.

Wo der Modulith schwach ist

Auch dieser Ansatz hat Kosten:

  • Die Datenbank als Hintertür. Ohne getrennte Schemata ist alles andere Kosmetik.
  • Regeln lassen sich aushebeln. Eine Architekturverletzung ist per Ausnahme in dreißig Sekunden „gelöst". Ob sie halten, ist eine Frage der Teamkultur.
  • Ein Prozess, ein Schicksal. Ein Speicherleck in einem Modul reißt die Instanz mit. Mehrere Replikas fangen den Ausfall ab, aber Isolation gegen einen fehlerhaften Nachbarn gibt es nur logisch, nicht auf Prozessebene.
  • Skalierung nur gemeinsam. Horizontal skaliert der Modulith problemlos, aber immer als Ganzes: Ein Modul, das zehnmal mehr Last hat als der Rest, zieht zehn Replikas von allem nach sich. Dazu ein gemeinsamer Release-Zug.

Modernisierung beginnt vor der Architekturentscheidung

Der Wunsch nach Verteilung kommt in Modernisierungsvorhaben fast immer aus einer realen Not, die sich präziser beschreiben lässt als mit „unser System ist zu groß": unklare Verantwortlichkeiten, Release-Abhängigkeiten, verlorenes Wissen. Wer diese Not zuerst benennt, entscheidet besser. Im Themenfeld IT-Transformation von adesso findet ihr dafür unter anderem Capability Maps, mit denen sich fachliche Zuschnitte und IT-Landschaft zusammenbringen lassen.

Fazit: erst entscheiden, dann verteilen

Ob ihr aus einem neuen Projekt, einem gewachsenen System oder einer heimatlosen Anforderung kommt: Die Frage ist dieselbe. Welches Problem löst ein eigener Service, und welche Probleme handeln wir uns damit ein? Wird sie nicht gestellt, entsteht Verteilung aus Gewohnheit. Und Gewohnheit ist die teuerste Form der Architekturentscheidung, weil sie niemand begründen muss.

Macht den Modulith zur Standardantwort und Microservices zur begründeten Ausnahme. Nicht, weil Verteilung falsch wäre, sondern weil sie eine Entscheidung verdient.

Bild Tino   Kiemann

Autor Tino Kiemann

Tino Kiemann ist seit 2022 bei adesso als Software Engineer tätig. Der Schwerpunkt seiner Arbeit liegt in der Entwicklung von Microservices. Darüber hinaus beschäftigt er sich intensiv mit DDD, CQRS, Event Sourcing und dem Java Framework Axon.


asdf

Unsere Blog-Beiträge im Überblick

In unserem Tech-Blog nehmen wir Sie mit auf eine spannende Reise durch die adesso-Welt. Weitere interessante Themen finden Sie in unseren bisherigen Blog-Beiträgen.

Zu allen Blog-Beiträgen

asdf

Unser Newsletter zum adesso Blog

Sie möchten regelmäßig unser adesso Blogging Update erhalten? Dann abonnieren Sie doch einfach unseren Newsletter und Sie erhalten die aktuellsten Beiträge unseres Tech-Blogs bequem per E-Mail.

Jetzt anmelden