Hexagonale Architektur

Lesezeit: 16 Minuten

Die hexagonale Architektur ist ein Architekturansatz, der eine Software in lose gekoppelte Komponenten unterteilt, die Technik von der Fachlichkeit trennt und die Austauschbarkeit der Komponenten erleichtert. Damit ist die hexagonale Architektur besonders geeignet für langlebige Softwaresysteme mit einer komplexen Fachdomäne.

Die Schichtenarchitektur

Der Wunsch in der Software-Entwicklung, Verantwortlichkeiten voneinander zu trennen, ist uralt. Der Begriff Separation of Concerns (SoC) ist ursprünglich das erste Mal im 1974 veröffentlichen Artikel On the role of scientific thought von Edsger W. Dijkstra gefallen. Die Schichtenarchitektur bildet die SoC in einer Software mit Benutzerschnittstelle und Datenspeicherung ab. Eine klassische 3-Schichten-Architektur kann wie folgt aussehen:

Im Presentation Layer befindet sich die Schnittstelle zum Benutzer. Die kann aus einer Web-GUI (HTML/JavaScript/CSS) und einem HTTP-Controller bestehen, oder aus einer nativen GUI und den dazugehörigen Controllern, z.B. Java Swing mit seinen Action Listenern. Im Business Layer ist die Geschäftslogik verankert und im Database Layer der SQL-Code.

Jede Schicht greift nur auf die ihr untergeordnete Schicht zu und die Datenbank ist das Zentrum der Anwendung. Diese Architektur hat einige Schwächen:

  • Die Geschäftsprozesse stehen nicht im Mittelpunkt der Anwendung.
  • Fachlicher Code muss angepasst werden, wenn sich die Technik in der darunterliegenden Schicht ändert.
  • Es gibt transitive Abhängigkeiten: Durch die Abhängigkeit jeder Schicht zu der darunterliegenden kann es passieren, dass der Presentation Layer angepasst werden muss, wenn sich der Database Layer ändert.

Warum Fachlichkeit?

Wie im Einleitungssatz geschrieben zielt die hexagonale Architektur auf eine komplexe Fachdomäne ab. Aber warum ist die Fachlichkeit so wichtig? Nun zunächst einmal würde ich behaupten, dass Fachlichkeit, sofern sie in einer Software vorhanden ist, die Existenzgrundlage dieser Software ist. Dem Fachbereich ist es egal, ob die Daten in einer OracleSQL– oder PostgreSQL-Datenbank abgelegt werden, ob die Schnittstellen zu anderen Systemen über RabbitMQ oder über SOAP abgebildet werden, und ob ein Redis verwendet wird um die Performance der Anwendung zu steigern oder ob dies einfach durch mehr Hardware-Ressourcen erreicht wird. Entscheidend ist, dass die Software die vom Fachbereich gewünschten und vorgegebenen Geschäftsprozesse abbilden kann und dass diese sich mit verhältnismäßigem Aufwand anpassen oder erweitern lassen.

Dafür ist es sinnvoll, Fachlichkeit und Technik konsequent voneinander zu trennen. Stellen wir uns einmal vor, wir würden dies nicht tun und auf den Business Layer verzichten. Nehmen wir an, wir implementieren einen kleinen Webshop. Wo bringen wir folgende fachliche Anforderungen unter?

  • Im Warenkorb dürfen maximal 10 Artikel liegen.
  • Der Gesamtwert im Warenkorb darf 2000€ nicht überschreiten.
  • Der Kunde darf nur auf Rechnung zahlen, wenn er in der Vergangenheit bereits 3x bestellt hat
  • Der Kunde muss bei der Registrierung zumindest einen Nachnamen angeben (Nachname darf nicht leer sein).

Die ersten beiden Anforderungen bringen wir im Presentation Layer unter, da wir dort bereits validieren können, ob diese Kriterien erfüllt sind. Für die dritte Anforderung ist eine Selektion auf der Datenbank erforderlich, wir bringen sie also im Database Layer unter. Die letzte Anforderung bilden wir in der Datenbank als Constraint direkt auf der Kundentabelle ab: Das Feld Nachname wird als not nullable deklariert.

Im Laufe der Jahre wächst die Anwendung weiter und es kommen neue fachliche Anforderungen hinzu. Unter anderem wird ein neuer Kundentyp „Firma“ eingeführt, der bis zu 50 Artikel mit einem Gesamtwert von 50000€ in den Warenkorb legen darf. Der Kundentyp wird im Database Layer abgefragt. Eine Fallunterscheidung wird in der Validierung im Presentation Layer eingebaut, um die Warenkorb-Validität sowohl für den Kundentyp „Privatkunde“ als auch den Kundentyp „Firma“ ermitteln zu können.

Spätestens jetzt zeichnet sich ab, dass das nicht auf Dauer gut gehen kann und die Anwendung Gefahr läuft, bei weiterem Wachstum zu einem Big Ball of Mud zu mutieren. Ist dies erstmal geschehen, sind massive Auswirkungen auf Qualität, Wartungs- und Entwicklungskosten die Folge. Entwickler trauen sich an bestimmte Code-Teile nicht mehr ran und bauen immer weitere Fallunterscheidungen ein. Es kommt zu Dead Code und Lava Flows.

Irgendwann, wenn die Wirtschaftlichkeit nicht mehr gegeben ist, wird die Entscheidung getroffen, die Software neu zu schreiben. Dadurch, dass jede Schicht von der ihr untergeordneten Schicht abhängig ist und die Fachlichkeit über alle Schichten verteilt ist, kann von der gewachsenen Software nichts übernommen werden und es wird auf der grünen Wiese neu begonnen. Eine Rekonstruierung der Geschäftsprozesse mit allen Randfällen aus der bestehenden Software scheint aufgrund der Verteilung und Lesbarkeit des Codes unmöglich. Sofern der Fachbereich die Prozesse nicht im Detail und auf dem aktuellen Stand dokumentiert hat, beginnt man auch dort auf der grünen Wiese.

Auf dem Weg zum Hexagon

Wie lässt sich die Schichtenarchitektur langfristig flexibler und wartungsfreundlicher gestalten? Durch die Anwendung zweier SOLID-Prinzipien, Dependency Inversion Principle (DIP) und Interface Segregation Principle (ISP).

Ein großer Nachteil der klassischen Schichtenarchitektur ist die engle Kopplung einer Schicht mit der darunterliegenden. Migriert man die Anwendung im Laufe der Zeit von einer SQL-Technologie hin zu einer anderen und nutzt dabei vom SQL-Standard abweichende Hersteller-spezifische Features, so wird dies zwangsweise Anpassungen im darüberliegenden Layer bedeuten. Die Lösung dafür ist das DIP: Die höherliegende Schicht macht sich nicht von der darunterliegenden Schicht abhängig. Stattdessen werden Abstraktionen verwendet. Diese Abstraktionen (Interfaces) müssen so gewählt werden, dass sie auch über eine Technologie hinaus Bestand haben, so dass z.B. eine SQL-Technologie durch eine andere ersetzt werden kann, ohne dass das öffentliche Interface im Database Layer dafür geändert werden muss.

Repräsentiert man eine Schicht durch ein riesiges öffentliches Interface, welches durch die darüberliegende Schicht angesprochen wird, verliert man irgendwann den Überblick, welche Interface-Methoden aus welchen Programmteilen heraus angesprochen werden. Bei der Umsetzung neuer Geschäftsprozesse im Business Layer werden dann irgendwelche möglichst passenden Interface-Methoden im Database Layer aufgerufen oder es werden einfach neue Methoden ins Interface aufgenommen. Nach eigener Erfahrung geht das langfristig nie gut und spätestens, wenn bestehende Mitglieder das Team verlassen und neue hinzukommen, weiß niemand mehr, welche Intention hinter welcher Interface-Methode steckt, wie man das Interface sinnvoll refactoren und welche Methoden man zusammenführen kann. Die Lösung dafür ist, durch die Anwendung des ISP eine hohe Kohäsion zu erreichen und jedem Anwendungsfall genau das Interface zu bieten, das dafür benötigt wird.

Die Graphik verdeutlicht den Einfluss der beiden Prinzipien auf die Schichtenarchitektur:

  • Innerhalb eines Layers kann es mehrere Vertikalen geben, um verschiedene Anwendungsfälle bzw. die Komponenten, die zueinander keinen Bezug haben, zu trennen.
  • Die Kommunikation mit der darunterliegenden Schicht erfolgt über ein auf den Anwendungsfall zugeschnittenes Interface.

Geschichte des Hexagons

Den Architekturansatz, der die Schichtenarchitektur mit den Vorzügen des DIP und ISP kombiniert, hat Alistair Cockburn Anfang der 1990er Jahre in einem Hexagon visualisiert, wobei die Anzahl der Seiten für den Architekturansatz nicht relevant ist und vom jeweiligen Szenario abhängt. Da der Begriff der hexagonalen Architektur eher als Arbeitstitel gedacht war und die Idee dahinter nicht optimal beschreibt, wurde der Ansatz im Jahr 2005 in Ports and Adapters umbenannt. Jeffrey Palermo hat 2008 außerdem den Begriff der Onion Architecture geprägt, welche sich von Ports and Adapters in Details unterscheidet. Im Wesentlichen verändert Ports and Adapters die oben skizzierte Schichtenarchitektur mit DIP und ISP insofern, als dass der Business Layer als Polygon in den Mittelpunkt gestellt wird und sich die verschiedenen Komponenten des Presentation und Database Layers als Ports und Adapter an dieses Polygon andocken.

Das Hexagon im Detail

Die hexagonale Architektur unterteilt eine Software in folgende Komponenten:

  • Driving Adapters
  • Driven Adapters
  • Application
  • Domain (Model und Services)

Die folgende Abbildung visualisiert die hexagonale Architektur:

Domain

Im Zentrum der hexagonalen Architektur steht die Fachlichkeit. Die Domain ist komplett technikfrei. Das bedeutet in einer Java-Anwendung beispielsweise, dass JSON- oder JPA-Annotationen in der Domain nichts verloren haben. Die Domain ist in sich geschlossen und hat keine Abhängigkeiten zu anderen Schichten. Unit-Tests in der Domain dokumentieren die fachlichen Anforderungen. In einer komplexen Fachdomäne ist der Einsatz von Domain-driven Design empfehlenswert. An dieser Stelle sei auf meine Mini-Reihe zu den Building Blocks im Domain-driven Design verwiesen, die sich hervorragend mit einer hexagonalen Architektur kombinieren lassen und dabei die komplette Domain-Schicht abbilden.

Die Domain enthält üblicherweise:

  • fachliche Logik
  • fachliche Daten
  • fachliche Berechnungen
  • fachliche Validierungen
  • fachliche Vor- und Nachbedingungen
  • fachliche Invarianten

Idealerweise ist die Domain so geschrieben, dass sie für den Fachbereich komplett verständlich ist, wenn der Fachbereich sich den Code mit einem Entwickler zusammen anschaut.

Application

Die Application orchestriert die Domain und enthält alle Aspekte der Anwendungsfälle, die nicht als Geschäftslogik verstanden werden. Was als Geschäftslogik zu verstehen ist, ist dabei abhängig von der jeweiligen Anwendung. Der Versand einer Benachrichtigungsmail nach Ablauf eines Geschäftsprozesses kann z.B. in der Application angesiedelt sein, je nach Betrachtungsweise kann er aber auch in der Domain sein. In einer klassischen Spring-Boot-Anwendung mit Java würde ich Server, Logging und Konfiguration im Application Layer unterbringen. Bei Zweifeln, ob eine Anweisung Teil der Domain ist oder im Application Layer untergebracht wird, würde ich mir und dem Fachbereich die Frage stellen, ob Änderungen an diesem Code fachliche Transaktionen kaputt machen bzw. die fachlich vorgegebenen Invarianten verletzten würden.

Ein Application Service arbeitet direkt auf der Domain, nicht auf anderen Application Services. Kaskaden von Application Services werden nach Möglichkeit vermieden. Die Application ruft keinen Adapter-Code direkt auf, dazu mehr im Abschnitt Driven Adapters.

Driving Adapters

Driving Adapters, auch bekannt als Primary Adapters oder Active Adapters wirken auf die Anwendung von außen ein. In der Abbildung weiter oben sind sie blau hinterlegt. Bei einem Driving Adapter kann es sich z.B. um einen HTTP-Controller oder einen Message Consumer handeln. Driving Adapters greifen immer nur auf Interfaces im Application Layer zu, nie auf konkrete Implementierungen und auch nicht auf andere Adapter.

Adapter haben häufig ihr eigenes Datenmodell, z.B. eine JSON-Repräsentation eines Domain Models oder eine AMQP-Nachricht, die ein Domain Model, ein Update auf dieses oder eine Löschanweisung repräsentiert. Ein Adapter stellt dafür einen Transformer bereit, der diese Adapter-spezifische Abbildung in die Domain übersetzt (oder andersrum). Sämtlicher Adapter-Code ist in sich geschlossen und wird nicht aus anderen Teilen der Anwendung heraus aufgerufen.

Driven Adapters

Driven Adapters sind Implementierungen eines im Application Layer oder der Domain angesiedelten Interfaces. Für den Mail-Adapter in der Abbildung weiter oben kann es z.B. ein NotifyCustomerAdapterPort-Interface im Application Layer geben. Der Name im Application Layer sollte fachlich motiviert sein. Ein SendMailAdapterPort wäre keine gute Wahl, da dies ein Refactoring notwendig macht, wenn eine rein technische Umstellung stattfindet, wie die Umstellung von einer E-Mail-Benachrichtigung auf eine SMS-Benachrichtigung.

Verifizierung der hexagonalen Architektur

Hexagonale Architektur in einer komplexen Anwendung ist nicht trivial und bietet nicht nur für Einsteiger viel Fehlerpotenzial. Es ist daher sinnvoll, die Einhaltung der Architekturregeln zu überwachen. Für Java-Anwendungen ist ArchUnit ein für diesen Zweck sehr geeignetes Werkzeug. ArchUnit erlaubt es die zugelassenen Beziehungen zwischen Klassen und Packages in Tests zu definieren und bietet auch direkte Unterstützung für Schichtenarchitekturen und die Onion Architecture, wobei letzteres auch eine hexagonale Architektur gut absichern kann.

Vor- und Nachteile der hexagonalen Architektur

Wie eingangs erwähnt zielt die hexagonale Architektur auf Anwendungen ab, bei denen die Geschäftsprozesse im Mittelpunkt stehen. Für Prototypen, Wegwerfsoftware, Durchlauferhitzer wie Proxies, Converter, Datenpumpen oder auch einfache CRUD-Anwendungen ist der hexagonale Ansatz wenig geeignet.

Davon ab bietet sich die hexagonale Architektur sowohl für Microservices als auch Monolithen an, vorhandene Fachlichkeit vorausgesetzt. Ein großer Mehrwert des Ansatzes ist die Trennung der Fachlichkeit von der Technik. Geschäftslogik kann unabhängig von der Infrastruktur kompiliert, deployed und wiederverwendet werden. Auch wenn der Lebenszyklus einer Anwendung irgendwann zuende geht, kann der Domain-Code mit verhältnismäßig geringem Aufwand ins Nachfolgeprodukt übernommen werden. Gut geschriebener und vor allem gut abgetesteter Domain-Code kann als Dokumentation dienen und sogar für technikfremde Fachbereichskollegen verständlich sein. Die hexagonale Architektur schafft eine zentralisierte Komplexität. Technische Adapter können mit wenig Aufwand ausgetauscht werden, wenn z.B. ein Dateiimport durch asynchrone Nachrichten ersetzt wird oder eine SOAP-Schnittstelle durch REST abgelöst wird. Der hexagonale Ansatz impliziert außerdem eine geordnete Struktur. Gerade im Microservice-Umfeld muss nicht jedes Team das Rad neu erfinden und eine für sich passende Package-Struktur finden. Die Aufteilung in Domain, Application und Adapters sorgt dafür, dass mit dem Ansatz vertraute Kollegen sich in einem neuen Projekt sehr schnell zurecht finden. Damit einher geht auch, dass es sofort ersichtlich ist, welche Schnittstellen zu anderen Systemen eine Anwendung bietet, denn diese manifestieren sich klar als konkrete Driving und Driven Adapters. Empfängt der Service AMQP-Nachrichten? Verschickt er sie? Welche Geschäftsprozesse sind dabei involviert? Alles transparent. Zu guter Letzt bringt der Ansatz auch eine bessere Testbarkeit mit sich, denn das DIP gewährleistet, dass alle Adapter, der Application Layer und die Domain jeweils unabhängig voneinander getestet werden können.

Welche Nachteile gibt es? Die hexagonale Architektur bringt zusätzliche Komplexität mit sich. Meiner Meinung nach zahlt sich das immer aus, wenn die Anwendung langlebig und fachlich komplex ist. Die Kosten, die eine sogenannte Legacy-Anwendung mit sich bringt, übertreffen den Overhead einer hexagonalen Architektur um ein Vielfaches. Von großer Bedeutung ist, dass alle an der Entwicklung Beteiligten den verfolgten Architekturansatz verstehen, vertreten und danach handeln. Eine nur halbherzig umgesetzte hexagonale Architektur kann ansonsten selbst zu einer Legacy-Anwendung führen. Erwähnenswert finde ich noch, dass nicht alle Frameworks gleichermaßen für diesen Architekturansatz geeignet sind. Wenn man JPA zur Datenspeicherung in einer Java-Anwendung verwendet und die hexagonale Architektur konsequent umsetzt, kommt man schnell zu einer doppelten Datenhaltung und konvertiert bei jeder schreibenden Aktion zwischen Domänenentität und JPA-Entität hin und her. Hier sollte man darüber nachdenken, ob man nicht einen Kompromiss eingehen möchte und JPA zum Teil der Domäne macht. Meines Erachtens ist das eine valide Option, da JPA überwiegend über Annotationen integriert wird und damit zumindest keinen störenden Einfluss auf die fachlichen Unit-Tests hat. Frameworks, die noch weniger mit der hexagonalen Architektur vereinbar sind, können einen zu der Entscheidung zwingen, ob man in der Anwendung lieber komplett auf hexagonale Architektur oder komplett auf den Einsatz dieses Frameworks verzichtet.

Mein persönliches Fazit in einem Satz: Je mehr Schnittstellen und fachliche Invarianten eine Anwendung hat, desto eher würde ich die hexagonale Architektur anwenden.

- Hexagonal architecture von Alistair Cockburn
- The onion architecture von Jeffrey Palermo
- On the role of scientific thought von Edsger W. Dijkstra