Hexagonale Architektur (Ports & Adapters)
Aufbau & Vorteile einfach erklärt
Wartbar, testbar, langlebig: Die Struktur deiner Software entscheidet über alles. Genau deshalb ist die Hexagonale Architektur — auch bekannt als Ports and Adapters — so einflussreich in der modernen Entwicklung.
Doch auf welcher Ebene der Gesamtarchitektur siedeln wir dieses Konzept eigentlich an? Und wie sieht das konkret im Code aus?
1. Wo steht die Hexagonale Architektur? (Ebene der Komponenten- und Modulararchitektur)
Um die Hexagonale Architektur richtig einzuordnen, wirf einen Blick auf die verschiedenen Architektur-Ebenen, wie sie im folgenden Schaubild dargestellt sind:
Wie du siehst, befindet sich die Hexagonale Architektur klar auf der Ebene der Komponenten-/Modulararchitektur.
- Frage dieser Ebene: Wie sind die Bausteine eines Systems intern strukturiert?
- Ziel: Wartbarkeit, Testbarkeit, klare Verantwortlichkeiten.
Das bedeutet: Die Hexagonale Architektur beschäftigt sich nicht damit, ob du Microservices oder einen Monolithen baust (Systemebene), und auch nicht damit, wie deine Services kommunizieren (Integrationsebene). Sie regelt ausschließlich, wie der Code innerhalb eines einzelnen Moduls oder einer Komponente organisiert ist. Sie sorgt dafür, dass dein Fachwissen (die Geschäftslogik) vom Rest der Welt isoliert wird.
Das Herzstück: Ports and Adapters
Die Grundidee ist einfach: Trenn die Geschäftslogik von der Infrastruktur.
Stell dir die Anwendung wie einen Hexagon (sechsseitiges Polygon) vor. Dieses Sechseck ist der Anwendungskern. Alles, was die Kernlogik zum Funktionieren benötigt, geschieht über klar definierte Schnittstellen — die sogenannten Ports. Die Infrastruktur (Datenbanken, Web-APIs, Message-Queues) wird über Adapter angeschlossen.
Ports (Schnittstellen)
Ein Port ist eine Schnittstelle (Interface), die definiert, was die Außenwelt vom Kern erwartet (Driving Port) oder was der Kern von der Außenwelt erwartet (Driven Port).
- Driving Port (Inbound): Die API, über die Benutzer mit der Anwendung interagieren. Definiert im Kern. Beispiel:
UserServiceInterface mit der MethodecreateUser(UserCommand). - Driven Port (Outbound): Die API, über die der Kern mit der Infrastruktur interagiert. Definiert im Kern. Beispiel:
UserRepositoryInterface mit der Methodesave(User).
Adapters (Implementierungen)
Adapter sind die Implementierungen, die diese Ports mit der jeweiligen Technologie verbinden.
- Driving Adapter: Implementiert den Driving Port. Er übersetzt externe Aufrufe (z.B. HTTP-Requests) in Aufrufe des Kerns. Beispiel: Ein
UserController(REST-Controller), der denUserServiceaufruft. - Driven Adapter: Implementiert den Driven Port. Er übersetzt die Anfragen des Kerns (z.B.
save(User)) in Infrastruktur-spezifische Aufrufe (z.B. SQL-Befehle oder MongoDB-Transaktionen). Beispiel: EineJpaUserRepositoryoderMongoUserRepository.
2.1 Inversion of Control: Der Schlüssel zur austauschbaren Infrastruktur
Der entscheidende Mechanismus, der diese Trennung ermöglicht, ist die Inversion of Control (IoC), genauer gesagt, das Dependency Inversion Principle (DIP).
Das Problem ohne IoC: Normalerweise würde der Kern direkt eine konkrete Implementierung kennen und nutzen:
// Schlecht: Der Kern ist von der Infrastruktur abhängig
public class OrderServiceImpl {
// Konkrete Abhängigkeit!
private JpaOrderRepository repository = new JpaOrderRepository();
// ...
} Die Lösung mit IoC/DIP: Der Kern definiert nur die Schnittstelle (den Port), während die Infrastruktur die Implementierung (den Adapter) bereitstellt. Der Kern bekommt die konkrete Implementierung von einem externen Mechanismus, dem IoC Container (z.B. Spring oder CDI), injiziert.
// Gut: Der Kern ist nur vom Interface (Port) abhängig
public class OrderServiceImpl implements OrderService {
// Dependency Injection (IoC)
private OrderRepository repository; // Abhängig vom Port-Interface
public OrderServiceImpl(OrderRepository repository) {
this.repository = repository;
}
…
}
Durch diese Abhängigkeitsinversion ist der Kern nicht mehr von der Technologie des Adapters abhängig. Die Infrastrukturschicht (der Adapter) ist es, die vom Kern abhängt, da sie dessen Port-Interface implementieren muss.
Der Clou beim Austausch:
Wenn du die Datenbank wechseln möchtest, musst du nur den Infrastruktur-Adapter austauschen:
- Alte Konfiguration:
OrderRepositorywird mitJpaOrderRepositoryverbunden. - Neue Konfiguration: Wir erstellen einen neuen Adapter, z.B.
MongoOrderRepository, und konfigurieren den IoC Container so, dass er nun diesen neuen Adapter an denOrderServiceübergibt.
Der gesamte Code im Kern (OrderServiceImpl) bleibt dabei unverändert, was die Wartung und Evolution des Systems erheblich vereinfacht.
3. Umsetzung in einer Java Package Struktur
In einer typischen Java-Anwendung führt dieses Muster zu einer klaren, viergeteilten Paketstruktur. Nehmen wir als Beispiel eine Anwendung zur Verwaltung von Kundenaufträgen:
Fazit: Die Vorteile der Isolation
Die Hexagonale Architektur ist der Königsweg zu wartbarer Software, da sie Isolation schafft:
- Leichtere Testbarkeit: Der gesamte Kern (
domainundapplication) kann ohne jegliche Datenbank- oder Netzwerkkonfiguration getestet werden. Wir müssen lediglich die Interfaces inports.outdurch Mock-Objekte ersetzen. - Technologiewechsel: Wenn du von MySQL auf PostgreSQL oder von REST auf gRPC wechseln musst, änderst du nur die entsprechenden Adapter in
adapter.outoderadapter.in. Der Anwendungskern bleibt unberührt. - Klarheit: Die Verantwortung ist klar verteilt: Der Kern weiß, was getan werden muss, die Adapter wissen, wie es technisch umgesetzt wird.
Indem du diese Trennung konsequent durchsetzt, stellst du sicher, dass dein wertvollstes Gut — deine Geschäftslogik — immer sauber, testbar und vom schnelllebigen Infrastruktur-Lärm entkoppelt bleibt.
