The Practical Majestic Monolith with DDD and DHH
Der gute alte Monolith. Heutzutage leider von vielen als “antipattern” verspottet und daher nicht wirklich beliebt. Buzzwords wie “Microservices” und “Service-Orientierte-Architektur” ziehen deutlich mehr Leute an, was komisch ist, da viele gar nicht wissen, warum diese Architekturtypen besser sein sollen. Meistens kommen dann die immergleichen Argumente wie “Skalierbarkeit” oder “Teamgröße”, meist ohne weitere Erklärung. Wieder nur Buzzwords.
Ich dachte lange Zeit auch zu wissen, was ein Monolith ist und warum man definitiv niemals einen bauen sollte. Starte ich jetzt aber ein neues Projekt oder komme in ein bestehendes, ist das Problem immer gleich: Verteilte Architekturen, egal welcher Art, haben eben durch diese Verteilung einen meistens unnötigen Overhead. Genau das war der Grund mich mit dem Thema Monolithen nochmal genauer auseinander zu setzen und du dich wahrscheinlich auch, sonst würdest du diesen Artikel nicht lesen.
Der ursprüngliche Artikel “The Majestic Monolith” wurde im Jahr 2016 von David Heinemeier Hansson (abgekürzt DHH, Entwickler von Ruby on Rails, Gründer und CTO von Basecamp) veröffentlicht, bleibt aber eher auf einer Metaebene.
In diesem Artikel geht es um explizite Gründe für den Monolithen und um konkrete Implementierungsmethoden, um saubere Monolithen zu bauen.
Definition
Aber was bedeutet es eigentlich ein monolithisches System zu bauen? Frei übersetzt definiert Wikipedia: "Ein Softwaresystem wird als ‘monolithisch’ bezeichnet, wenn es eine monolithische Architektur hat, in der funktional unterscheidbare Aspekte [...] alle miteinander verwoben sind, anstatt architektonisch getrennte Komponenten zu enthalten." (Stand: Juni 2021) Das klingt tatsächlich nicht gut und ruft im Kopf eher das Bild eines “Big ball of mud” hervor.
Das Problem mit dieser Definition ist, dass sie schlicht falsch ist.
Die Architektur eines Monolithen lässt sich nicht per se als schlecht bezeichnen. Monolithische Architektur bedeutet auch nicht, dass keine architektonischen Komponenten vorhanden sind, sondern nur,dass diese nicht verteilt sind.
Und mit genau dieser differenzierteren Definition können wir uns anschauen, warum der Monolith so gut ist.
Tell me why
Schon DHH schrieb in seinem Artikel: “#1 rule of distributed computing: Don’t distribute your computing!”. Der Grund ein System zu verteilen, sollte nicht sein, dass man es kann. Durch Entwicklungen wie Cloud Computing und Kubernetes wird es uns zwar einfacher gemacht auch in kleineren Projekten verteilte Systeme zu managen, aber deswegen ist es nicht sinnvoller. Wer einmal “word cloud distributed systems” gegoogelt hat, bekommt einen Eindruck davon, welche Komponenten in einem System zu beachten sind.
Natürlich spielen diese Komponenten auch bei der Entwicklung von nicht verteilten Systemen eine Rolle, werden aber in verteilten deutlich komplexer. So wird beispielsweise der Faktor Sicherheit zwischen den einzelnen Services meist unterschätzt.
Ein weiterer großer Vorteil des Monolithen ist, dass EntwicklerInnen alles was sie brauchen an einem Ort haben. Mit den zwei einfachen Befehlen “make setup” und “make start” läuft das gesamte System bei jedem einzelnen. Und noch viel wichtiger: EntwicklerInnen verstehen, wie das System funktioniert, ohne sich durch komplizierte yaml-Files für das Kubernetes-Cluster zu kämpfen.
Das wohl wichtigste Argument an dieser Stelle ist aber folgendes: Das Entwicklungsteam sollte nie der Grund sein, das System zu skalieren. Leider ist oft das Argument, warum Systeme verteilt werden, dass EntwicklerInnen “sich gegenseitig auf den Füßen stehen”. Selbstverständlich ist der Grund, warum sich EntwicklerInnen gegenseitig blockieren, nicht, dass sie an einem Monolithen arbeiten, sondern weil die Architektur des Monolithen fast immer verrottet ist. Verteilen wir diesen Monolithen jetzt, lindert das den Schmerz vielleicht kurzfristig, sorgt aber dafür, dass sich die einzelnen verrotteten Teile das Monolithen auf unzählige Services verteilen. Und diese werden der Erfahrung nach genauso weiterbetrieben.
Um genau diesen sauberen Schnitt hinzubekommen, gibt es schon genug Methoden da draußen. Diesbezüglich brauchen wir das Rad zum Glück nicht neu erfinden.
Let’s implement
Starten wir mit Standards: Egal wie viele EntwicklerInnen im Code arbeiten, jeder Teil sollte immer aussehen als wäre er von ein und derselben Person geschrieben worden. Dabei helfen verschiedene Workshop-Formate, um beispielsweise Naming- oder Qualitätsstandards zu definieren.
Dann müssen wir natürlich noch die Komplexität managen, was wir am besten über Tests im allgemeinen oder mit Hilfe spezieller Architekturtests beziehungsweise Fitness Functions machen.
Dazu kommen Standardtools wie Error-Logging oder Monitoring Anwendungen.
Aber der interessanteste Teil kommt genau jetzt: Die Anwendung des Domain-Driven-Designs (DDD) in Zusammenhang mit Monolithen. Das klingt dem ersten Anschein nach gegensätzlich, denn DDD wird meist nur in Verbindung mit “Microservices” und “Service-orientierter Architektur” genutzt oder als Methode, um Monolithen zu zerschneiden. DDD eignet sich aber auch perfekt, um einen Monolithen so zu gestalten, dass er eben nie zerschnitten werden muss.
In unseren Projekten starten wir dazu gerne mit dem Event-Storming: So lässt sich herausfinden, was eigentlich in unserem System passiert und welche Kontexte und Aggregate (Entities) unser System wahrscheinlich enthalten wird. Mit dem Event-Storming als Grundlage können wir mit dem sogenannten “tactical Design” weitermachen. Damit definieren wir die Kontexte und Aggregate unseres System, wie im unten dargestellten Beispiel.
Entscheidend ist, dass alle Aggregate, wie beispielsweise Antrag oder Bürgschaft, vollkommen eigenständig sind. Dazu teilen wir auch in unserer Ordnerstruktur jeweils die Kontexte und die Aggregate in eine Ordnerebene ein. Innerhalb dieser Aggregate ziehen wir dann die aus dem DDD bekannten Layer ein. Das sieht bezogen auf unser Beispiel so aus:
Das bedeutet, dass jedes Aggregat je einen Ordner für die Interface, Application und Domain Layer enthält. Mit dieser Struktur lassen sich nicht nur die Aggregate und Kontexte sauber voneinander trennen, sondern auch die Komplexität wird gering gehalten. Sowohl die Trennung zwischen Kontexten und Aggregaten, als auch die Hierarchie der Layer lässt sich über Architekturtests ganz einfach abbilden. Und um an der Stelle absolut sauber zu bleiben, findet die Kommunikation zwischen den Aggregaten ausschließlich über technische Interfaces statt, die ähnlich wie APIs auf die Controller des anderen Aggregates zugreifen:
Lessons Learned
Noch einmal die Learnings zusammengefasst:
- Monolithen können genauso modular, skalierbar und wartbar sein wie M/SOA und wir haben gelernt wie.
- Monolithen haben den großen Vorteil, dass sie nicht verteilt werden, und wir haben gelernt, warum.
- Monolithen sind auch für große Teams praktisch und wir haben gelernt, weshalb.
Und um die dramaturgische Klammer zu schließen noch ein Zitat von DHH: “Die Muster, die für Organisationen sinnvoll sind, die um Größenordnungen größer sind als Ihre, sind oft das genaue Gegenteil von dem, was für Sie sinnvoll ist.”