Tech Talk: How LASSSIM built: "Hexagonal Architecture" mit Simon Lasselsberger

Tech Talk: How LASSSIM built: "Hexagonal Architecture" mit Simon Lasselsberger

Hallo, ich bin Simon Lasselsberger, ich bin Softwarearchitektur- und Development-Consultant seit 2 Jahren und habe davor fast 9 Jahre bei Runtastic gearbeitet als Lead System Architect.

In meine Arbeit taucht eine Anforderung immer auf, Geschwindigkeit.

Nicht nur die Geschwindigkeit der Software selbst, sondern auch wie schnell kann man sie entwickeln kann, wie schnell kann man Fehler finden und wie schnell man die Software auch erweitern kann. Natürlich, die Skalierbarkeit ist auch immer ein großer Faktor.

Mit ein paar wenigen Regeln der Software Architektur schafft man es aber, ein System zu bauen, welches all diesen Regeln entspricht. Damit wir die hexagonale Architektur, die mir sehr am Herzen liegt, besser verstehen kann, müssen wir uns vorher über Pattern unterhalten – wie man denn eigentlich größere Softwaresysteme für Web Anwendungen oder mobile Applikationen baut.

Da gibt es eigentlich ein Pattern, das allgegenwärtig ist: die logische Trennung zwischen Client- und Backend-Code. Aber um das geht es mir jetzt gar nicht so, ich möchte eher den Fokus auf die Backend Architektur richten.

Hier ist die Trennung jedoch nicht so klar. Es gibt keine eindeutigen Empfehlungen, wie man ein Backend bauen soll. Aber es gibt zwei Richtungen, die man gehen kann: Einerseits die monolithische Architektur, andererseits die Microservices-Architektur.

Schauen wir uns grob einmal einen Monolithen an. Hier sind alle Features in einem Artefakt, welches deployed wird, zusammengefasst. Zum Beispiel Users, Newsfeed, Friends – all dies ist in einem Artefakt und wird miteinander deployed. Das hat Vorteile, weil man einfach sehr schnell eine Anwendung von 0 weg programmieren kann, man kann sie sehr schnell deployen, man kann sehr schnell recht gute Ergebnisse erzielen. Man kann auch gewisse Shortcuts eingehen. Weil man ja die gesamte Datenbank zur Verfügung hat, kann man zum Beispiel auch Statistik-Services überlegen, wo man ganz viele Tabellen über Joints verbindet und irgendwelche Insights generiert.

Je größer und älter aber so ein Monolith wird, desto mehr solche Shortcuts schleichen sich ein. Der Code wird leicht unstrukturiert, wenn man sich nicht wirklich an der Nase nimmt und da speziellen Fokus darauf legt. Der Monolith wachst, es gibt immer mehr Codeteile und das kann mitunter auch zum Problem werden. Das ist auch der Punkt, wo dann auch der Monolith tendenziell immer schwerer und schwerer zu testen wird. Das ist meistens so ein schleichender Übergang. Man kommt dann irgendwann mal an den Punkt, wo man erkennt dass alles etwas zu reiben beginnt – hier fangen dann die Developer an, über einen Rewrite nachzudenken. Das ist aber ein doppeltes Problem. Das heißt ja, dass die Developer, die jetzt damit arbeiten, nicht mehrganz zufrieden sind, aber nicht wissen wie sie die Codebasis handlen sollen. Andererseits ist es auch für neue Mitarbeiter schwierig, dass sie mittendrin einsteigen und mitwirken können an dem Ganzen.

Microservices Architektur kann hier ein bisschen Abhilfe bieten, weil man ist gezwungen, dass man den Code besser strukturiert und organisiert. Es lassen sich Teile leichter skalieren, als wie beim Monolithen – wenn ein Teil beim Monolithen langsam läuft, hat das Effekt auf den Rest des Systems. Auch Fehler wirken sich – wenn man es richtig macht – nur in einem kleinen Bereich aus, hier spricht man von einem geringeren „Blast Radius“. Zum Beispiel, wenn ein Statistik Service eine schlechte Performance hat, dann können in einer Microservice Architektur trotzdem Registrierungen oder andere Use Cases funktionieren. Bei einer monolithischen Architektur könnte aber die gesamte Applikation beeinträchtigt sein.

Wie vorher gesagt, der Monolith ist relativ einfach zu deployen, bei Microservices hingegen ist das etwas schwieriger. Man muss bei den Microservices auch noch auf ein paar andere Sachen aufpassen. Man muss etwa genau herausfinden können, WIE man das System in verschiedene Aspekte aufteilt – im Domain Driven Design spricht man hier von „Bounded Context“.

Was natürlich auch noch ein weiterer Gesichtspunkt bei der Zerlegung eines Systems in Microservices ist, ist das Conway’s Law. Es besagt, dass die Kommunikationswege im Unternehmen sich auch in der Softwarearchitektur wiederfinden sollen. Das soll so viel heißen, wie dass ein Microservice auch nur von einem Team verantwortet werden soll. Das soll nicht heißen, dass nur dieses eine Team Änderungen vornehmen darf, aber das Team muss die Verantwortung darüber haben. Es gibt hier keine richtige shared Codebase. Das Ziel von Microservice-Architektur ist es, Funktionalität zu isolieren und zwar auf allen Ebenen: in den Teams, auf Codebasis und im Betrieb. Hier ist es auch so, dass jedes Microservice seinen eigenen Datenbank-Cluster haben sollte, damit diese Unabhängigkeit gegeben ist.

Schauen wir uns einmal dieses Deployment genauer an. Die Komplexität des Deployments bei Microservices werden recht deutlich, wenn man eine Web Anwendung hat, die man redundant deployen will. Bei einem Monolithen braucht man – sagen wir mal – zwei Webserver und einen Datenbankcluster auf drei Servern (wenn ich Server sage, dann schließe ich auch Container oder Virtual Machines ein). Die gleiche redundante Ausführung bei Microservices hingegen würde acht Webserver und neun Datenbankserver fordern. Das zeigt die grundsätzliche Komplexität im Deployment von Microservices ganz gut.

Unterm Strich sind aber Microservices aber für viele Anwendungen eine gute Sache, sie werden schlichtweg komplexer. Monolithen werden auch komplex, hier aber eher zum Nachteil.

Wenn wir aber jetzt den Blick auf ein einzelnes dieser Softwarekomponenten von einem Microservice legen, dann kommt die Hexagonale Architektur ins Spiel. Da hat Alistair Cockburn gesagt:

„Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.“

Das soll also heißen, dass unser Ziel sein sollte, die eigentliche Aufgabe von einem Microservice getrennt von der Außenwelt zu betrachten und zu entwickeln. Die Kommunikation zu anderen Komponenten darf nur durch sogenannte „Adapter“ passieren. Die API Endpunkte, Datenbankzugriffe oder ausgehende Requests auf andere Microservices oder Komponenten sind über solche Adapter zu realisieren.

Da kommt ein weiteres Pattern ins Spiel: das Repository Pattern. Das kann sehr hilfreich sein, um eben genau diese Adapter zu realisieren.

Man könnte es so betrachten, das ein Hexagon einen API Gateway repräsentiert, dessen Business-Logik die Authentifizierung und das Routing von diesem API Request ist. Das wird weitergeleitet durch ein Repository auf das nächste Microservice. Der Vorteil von so einem Repository Pattern ist, dass man für Testzwecke eine Fake-Implementierung von dem Repository baut, um die Tests schnell zu halten. Dieses Pattern kann man beliebig oft verschachteln. Die Idee ist, die Business Logik getrennt zu halten. Der Business Logik ist es egal, ob sie von einer API Request aufgerufen wird oder von einer Konsolenapplikation. Das Gleiche geht auch in Richtung Repository – wenn die Business Logik irgendwo Daten anfordert, oder Kommunikation zu einem externen System herstellt, dann muss all dies durch einen Adapter durchgehen.

Der Business Logik ist es grundsätzlich wichtig, eine Aktion auszuführen, aber es ist nur sekundär wichtig, wie das passiert. Das Wie ist dann ist im Repository gekapselt.

Diese Kombination aus Microservices Architektur und der Hexagonalen Architektur ist eine tolle Sache, da Klarheit geschafft wird. Die oben geforderten Anforderungen werden auch unterstützt.

Microservices sind wie gesagt auch nicht pauschal das Richtige für alle Anwendungen, da sie ja eine gewisse Komplexität innebürgen.

Man kann aber dieses Hexagonale Architekturpattern auch wiederverwenden in einem Monolithen. Man überlege sich, dass wir schnell in den Markt müssen. Etwas gehört schnell entwickelt und wir möchten nicht die Komplexität einer Microservices Architektur auf der Deploymentseite. Aber wir wollen trotzdem eine klare Struktur in dem Monolithen. So kann man den Ansatz wählen, dass gewisse Softwarekomponenten definiert werden, die möglichst vielen Regeln der Microservices Architektur (Isolierung der Funktionalität, getrennte Datenbanken, usw.) implementieren, innerhalb des Monolithen. Dadurch bekommt man einen Monolithen, der nicht irgendwann einmal zum Big Ball of Mud wird, sondern im Vorhinein klar strukturiert ist und vorbereitet ist darauf, dass man gewisse Teile extrahieren kann. Man muss ja nicht gleich so weit gehen und den kompletten Monolithen auseinanderreißen, sondern man kann nur gewisse Aspekte trennen, zum Beispiel den Newsfeed in ein eigenes Microservices geben und unabhängig davon den restlichen Monolithen betreiben.

 

Wichtig ist dabei:

  • Funktionalitäten zu erkennen mithilfe des „Bound Context“ aus Domain Driven Design
  • immer über Adapter mit diesen Funktionalitäten zu kommunizieren
  • die Datenbanken voneinander trennen und sich keine so Shortcuts wie Joints zu erlauben

Es gibt noch weit mehr Aspekte von Microservices Archetiktur und Monolith Architektur, aber die Hexagonale Architektur kann man immer wieder verwenden, immer wieder wo einbauen und es bietet einfach klare Strukturen. Ich setze das sehr gerne ein für fast jegliche Architektur.

Man kann sich das auch so vorstellen – von einem höheren Level her betrachtet – dass ein Microservices System auch selbst eigentlich wieder ein Hexagon ist, mit Adaptern und so weiter. Wichtig ist aber immer diese klare Trennung der Funktionalität, die nicht direkt nach außen geteilt wird, sondern immer über Adapter läuft.

Wenn ihr noch mehr wissen wollt zu meiner Arbeit, oder vielleicht auch zur Runtastic Architektur – da habe ich zwei Conference Talks online, die könnt ihr auf meinem LinkedIn Profil finden. Für mehr Informationen oder Fragen zu diesem Thema kann man mich gern zu diesem Thema erreichen.

Ich wünsche euch alles gute und bleibt gesund!