Bevor wir uns mit Implementierungen und Architekturen beschäftigen will ich einen sehr kurzen, allgemeinen Umriss meines Projektes geben: Ich will die Fachlichkeit und die Ausgangslage darstellen und welche Gedanken wir uns dazu gemacht haben.
Fachlich gesehen handelt es sich um ein Archivierungstool. Anwendern sollte ermöglicht werden, Papierdokumente, die sich in den vergangenen Jahrzehnten angesammelt hatte, zu scannen, auf Gültigkeit zu prüfen, mit Metadaten zu versehen und dann revisionssicher in einem Archivsystem abzulegen. Da es sich um ein öffentliches Unternehmen handelt, gelten dort strengere Regeln zur Gewährleistung einer korrekten Ablage und Auffindbarkeit von Daten sowie zur fristgerechten Löschung oder Anonymisierung sensibler Informationen.
Eine Herausforderung hierbei waren jedoch die verschiedenen Archive innerhalb des Unternehmens; Strukturen, die über Jahrzehnte gewachsen sind und sich nicht in einer großen Hauruck-Aktion vereinheitlichen lassen. Daher standen wir vor der Herausforderung, einen strukturierten Prozess bereitzustellen, der den gesetzlichen Anforderungen genügt, und zugleich flexibel genug ist, auf die Besonderheiten jedes Fachbereichs eingehen zu können.
Die Geschichte dieser Anwendung ging bereits Jahre zurück und verschiedene Varianten wurden implementiert und wenig später wieder verworfen. Dem wollten wir mit unserem nachhaltigen und erweiterbaren Konzept entgegenwirken.
Was ist hexagonale Architektur?
Ich will einen groben Abriss über Hexagonale Architektur (HA) geben, ohne den Fokus auf unsere Erfahrungen mit HA jedoch aus den Augen zu verlieren.
Die gröbste Staffelung der HA sind ihre drei Schichten:
Domain mit Kernlogik in Entities, Value Objects und Domain Services
Applikation mit Use Cases und Ports
Infrastruktur mit Adaptern
Hexagonale Architektur im Überblick
Im Zentrum jedes Hexagons steht die ‚Entity‘: Business-Objekte, die die Assets eines Kunden darstellen. Hierbei kann man sich klassische Beispiele wie Bankautomaten, Autos, etc. vorstellen. In unserem Projekt finden sich dort „Akten“, „Dokumente“, „Anlagen“ wieder.
Auf diese Entities wird mittels Use Cases zugegriffen. Die dort enthaltene Logik kann Entities manipulieren, verknüpfen, löschen, etc. Komplexere Use Cases können auch elegant aus mehreren Use Cases zusammengestellt werden. Bei einer Use-Case-Komposition führt ein erster Geschäftsprozess seine Kernaufgabe vollständig aus, beispielsweise die Finalisierung von Akten nach Ablauf ihrer Aufbewahrungsfrist. Anschließend stößt dieser Prozess einen weiteren, fachlich eigenständigen Use Case an, wie etwa die Benachrichtigung der zuständigen Mitarbeiter, ohne dessen technische Umsetzung kennen zu müssen.
Die Use Cases werden nach außen durch Ports repräsentiert, sie stellen die Schnittstelle zur „Außenwelt“ für die Use Cases dar. Will ein Adapter auf einen Use Case zugreifen, so geschieht dies mittels des entsprechenden Input Ports. Im Falle von Java ist dies ein Interface, das vom Use Case implementiert wird. Will ein Use Case beispielsweise auf einen Wert in einer Datenbank zugreifen, so tut er dies mittels eines Output Ports, hinter dem dann ein Datenbank-Adapter versteckt liegt.
Dies stellt sicher, dass die Applikations-Schicht, in der die Use Cases liegen, von der äußeren Adapter- oder Framework-Schicht getrennt ist.
Während innerhalb der Applikations- und Domain-Schicht weitestgehend technologieagnostisch implementiert wurde (abgesehen natürlich von der verwendeten Programmiersprache), wird in der Adapter-Schicht auf konkrete Technologien eingegangen.
Beispiele: Soll die Applikation über eine API verwendet werden können? Dann wird ein RESTful-API-Adapter angelegt. Soll die Applikation auf eine Oracle Datenbank zugreifen? Dann implementiert ein Oracle DB Adapter einen Output Port.
Das Design gewährleistet, dass bestimmte Technologien (zum Beispiel konkrete Datenbanken) nie ins Zentrum der Applikation rutschen, sondern ein Implementierungsdetail bleiben, das zur Not auch schnell ausgetauscht werden kann.
Und das in einem Monolithen?
Liest man, was HA schaffen will und wie es das versucht zu tun, so fallen einem direkt Micro- oder Miniservices ein. Die Aufteilung in unabhängige Hexagone bietet sich direkt an, in einzelne, kleine Applikationen gesteckt zu werden.
Warum haben wir das nicht gemacht? Zum einen ist die Antwort trivial: Weder wurde es vom Kunden gewünscht, noch ist es bislang im Unternehmen umgesetzt worden. Zum anderen: Die Anzahl der Anwender, die Auslastung der Applikation und die zu erwartende Skalierung in der Zukunft machen die zusätzliche Ebene der Microservices unnötig kompliziert.
Warum haben wir uns dennoch für HA entschieden?
Weil uns nach der Aufbereitung der Fachlogik schnell klar wurde, dass es zwei sehr gut trennbare Bereiche in der Applikation geben wird. Zum einen die Kernlogik: Dinge, die jedes Archiv leisten können muss, wie zum Beispiel gesetzliche Anforderungen an Dokumentierbarkeit, Sicherstellung von Löschfristen, und Ähnliches. Zum anderen gibt es aber hochspezielle Logiken, die nur für jeweils eins der Archive gelten werden. Diese wollten wir strikt voneinander trennen, um eine leichte Einbindung von zukünftigen Archiven zu gewährleisten und die Entfernung obsoleter Archive einfach zu ermöglichen.
Wie haben wir es umgesetzt?
Ich möchte den Tech-Stack erläutern, in dem wir uns bewegt haben:
Unsere Applikation war und ist eine Java Backend-Applikation. Es handelt sich um eine Spring-Boot Applikation mit Anbindung an eine Oracle-Datenbank. Im Rahmen der Neugestaltung der Anwendung haben wir hier direkt Liquibase in den Stack aufgenommen, um zukünftige DB-Änderungen zu dokumentieren und zu vereinfachen.
Unsere Anwendung wurde mittels RESTful-API anderen Applikationen zugänglich gemacht.
Da unsere Applikation eine Archivierungssoftware darstellt, brauchten wir Zugriff auf ein Alfresco-System. Darüber hinaus haben wir andere RESTful-APIs eingebunden.
Herausforderungen und Learnings
Abseits der eigentlichen Implementierung war die größte Herausforderung, alle Entwickler an Bord zu bringen. HA ist weder alltäglich noch trivial, daher war die erste Schwierigkeit, alle Beteiligten auf den gleichen Wissensstand zu bringen. Auch wenn HA eine konkrete Architektur ist, so lässt sie doch genug Spielraum, um individuelle Gestaltungsprinzipien zu erlauben.
„Wird jede Klasse, die einen Use Case darstellt, mit UseCase Suffix versehen, oder sollte der Name für sich sprechen?“, „Ist das hier bereits ein Use Case oder eine Operation, die in der Entität hinterlegt werden sollte?“ – Das sind nur wenige der Fragen, die sich uns bei der Entwicklung gestellt haben.
Wir haben schnell gelernt, dass neben den obligatorischen style guides und code conventions auch ein Check für die Architektur an sich nötig ist. In unserem Projekt haben wir wie oben erwähnt dafür ArchUnit eingesetzt, das die gröbsten Regeln für uns im Auge behält.
Als diese Hürden genommen und erste Beispiele implementiert worden sind, beschleunigte sich die Implementierung sehr schnell.
Fragen beschränkten sich dann meist auf sehr konkrete Edge-Cases. „Sollte ein Scheduler im Adapter- oder im Use-Case-Layer sitzen?“, um nur ein Beispiel zu nennen.
Alles in allem haben wir jede Frage genutzt, um die Architektur im Allgemeinen zu festigen und die Regeln sinnvoll zu erweitern.
Fazit
Hat sich aus unserer und Anwendersicht der Umbau gelohnt? Auf jeden Fall! Natürlich gab es – wie bei jedem Projekt – Startschwierigkeiten und Wachstumsschmerzen. Unterm Strich ist es jedoch gelungen, eine Anwendung, die als Sorgenkind galt, mehrfach neu implementiert wurde und als nötiges Übel gesehen wurde, in ein wartungsfreundliches und flexibel erweiterbares System umzuwandeln.
Auch das Feedback der Anwender war sehr positiv – insbesondere, weil durch die Neugestaltung der Domain-Entitäten ein enger Austausch entstand. Dieser vermittelte das Gefühl, „dass sich gekümmert wird“ – ein psychologischer Aspekt, den ich an dieser Stelle bewusst hervorheben möchte.
Fragen oder Anregungen? Ich freue mich auch Euer Feedback.
Christian Schmid - Software Entwickler. Architekt. Teamplayer. Coach.
Ich entwickle nicht nur seit 10 Jahren Software – ich sorge auch dafür, dass Teams reibungslos zusammenarbeiten.
Mit dem Model-Context-Protocol (MCP) entsteht ein offener Standard zur Integration von KI-Modellen und externen Tools. Der Artikel beleuchtet die Struktur, Anwendung und Vorteile von MCP – und zeigt, wie Entwickler damit moderne, kontextbewusste KI-Systeme effizient gestalten können.
Fast jede App braucht eine Datenbank - Postgres ist eine der beliebtesten Open-Source-Datenbanken. In diesem Artikel zeigen wir, wie man Postgres in Kubernetes automatisiert mit dem Postgres Operator betreiben kann.