Mit wachsender Komplexität von Softwareprojekten wird die strukturierte Organisation des Codes zur zentralen Herausforderung. Traditionell haben sich für größere Anwendungen Schichtenarchitekturen etabliert, die eine klare Trennung zwischen Präsentation, Geschäftslogik und Datenzugriff schaffen sollten. In der Praxis führt diese Herangehensweise jedoch oft genau dazu, dass technische Abhängigkeiten alle Schichten durchdringen und die eigentliche Fachlogik von Infrastruktur-Entscheidungen dominiert wird.
In unserem Architektur-Circle befassen sich unsere Xperten u. a. mit modernen Softwaredesign-Ansätzen und bringen ihre Expertise ein, damit Projekte langfristig wartbar und flexibel bleiben. Ein besonders bewährtes Muster dabei ist die hexagonale Architektur von Alistair Cockburn – auch bekannt als „Ports and Adapters“ – die eine fundamentale Umkehrung der Abhängigkeitsrichtungen bewirkt: Die Geschäftslogik steht im Mittelpunkt, während alle technischen Details über definierte Schnittstellen nach außen abstrahiert werden.
Diese Architektur adressiert ein zentrales Problem moderner Softwareentwicklung: Wie können wir fachliche Anforderungen implementieren, ohne uns von technischen Entscheidungen einschränken zu lassen? Wie schaffen wir Software, die über Jahre hinweg flexibel anpassbar bleibt?
Warum klassische Schichtenarchitekturen an ihre Grenzen stoßen
Um den Mehrwert der hexagonalen Architektur zu verstehen, lohnt ein kurzer Blick auf die klassischen Schichtenmodelle – und darauf, warum diese in der Praxis oft an ihre Grenzen stoßen.

Die weit verbreitete Schichtenarchitektur mit ihrer dreistufigen Aufteilung in Präsentation, Geschäftslogik und Datenzugriff führt in größeren Projekten oft zu Problemen:
- Technische Abhängigkeiten durchdringen alle Schichten: Greift die Geschäftslogik direkt auf die Datenzugriffsschicht zu, entstehen Abhängigkeiten zu technischen Details, die die Fachlichkeit beeinflussen. Häufig zeigt sich das, wenn Datenbankmodelle direkt in der Geschäftslogik genutzt werden. Diese Modelle sind meist nach den Vorgaben der Persistenzschicht entworfen – z. B. durch Anforderungen von Frameworks wie JPA oder Hibernate, die öffentliche Getter und Setter erzwingen. Dadurch können fachliche Invarianten unterlaufen werden, und die Logik wird unnötig an konkrete Datenbank-Technologien gekoppelt.
- Schichtengrenzen weichen auf: Unter Zeitdruck neigen Entwickelnde dazu, die Grenzen zwischen den Schichten zu verwischen. Dann kann es passieren, dass Fachlichkeit in der Präsentationsschicht landet oder technische Details in die Geschäftslogik einfließen. Der Code der Geschäftslogik wird dadurch zunehmend unübersichtlich, wichtige Aspekte sind schwerer erkennbar und können bei technischen Anpassungen leicht verloren gehen.
- Testbarkeit leidet: Geschäftslogik lässt sich nicht isoliert ohne technische Abhängigkeiten wie Datenbanken testen.
Das hexagonale Prinzip: Geschäftslogik im Zentrum
Genau an diesen Schwachstellen setzt die hexagonale Architektur an und dreht die Perspektive um: Die Geschäftslogik steht im Mittelpunkt, nicht die Daten. Alle technischen Details werden über definierte Schnittstellen – die sogenannten Ports – nach außen abstrahiert und durch Adapter implementiert. Ports sind dabei Teil der Geschäftslogik und werden somit auch fachlich benannt.
Die Kernidee: Die Anwendung definiert, was sie benötigt (Ports), aber nicht, wie es implementiert wird (Adapter). Dadurch entstehen klare Abhängigkeitsrichtungen: Alle Abhängigkeiten zeigen nach innen zum Anwendungskern. Um das Prinzip greifbarer zu machen, wird zwischen primären und sekundären Ports unterschieden – je nachdem, ob die Anwendung gesteuert wird oder selbst andere Systeme ansteuert.

Anwendung
In der Praxis bedeutet das: Die Geschäftslogik stellt ihre Use Cases über primäre Ports bereit und nutzt sekundäre Ports, um externe Systeme wie bspw. Datenbanken oder Payment-Dienste anzubinden. Dabei bleibt sie vollständig frei von technischen Abhängigkeiten.

Beispiel 1:
// primärer Port
interface UserRegistrationUseCase {
fun registerUser(user: User): User
}
// sekundäre Ports
interface UserRepository {
fun add(user: User): User
fun existsByEmail(email: String): Boolean
}
interface UserNotificationService {
fun sendWelcomeMessage(user: User)
}
// Geschäftslogik
data class User(
private val id: String,
private val name: String,
private val email: String
) {
companion object {
fun create(name: String, email: String): Appointment {
require(!name.isBlank(), „name must not be blank“)
require(isMailAddress(email), „email must be a valid mail adress“)
return User(UUID.randomUUID().toString(), name, email)
}
}
}
class UserRegistrationService(
private val userRepository: UserRepository,
private val userNotificationService: UserNotificationService
) : UserRegistrationUseCase {
override fun registerUser(user: User): User {
check(!userRepository.existsByEmail(user.email), "user with that email is already registered")
val newUser = userRepository.add(user)
userNotificationService.sendWelcomeMessage(newUser)
return newUser
}
}
Adapter – Schnittstellen zur Außenwelt
Adapter setzen die zuvor definierten Ports technisch um. Sie übersetzen die fachlichen Schnittstellen der Geschäftslogik in konkrete Technologien – etwa REST-Controller, Datenbankzugriffe oder Integrationen zu externen Diensten. Damit bleibt die Fachlogik selbst unverändert, auch wenn sich die Infrastruktur oder eingesetzte Frameworks im Laufe der Zeit ändern.
Primäre Adapter – Die Steuerung der Anwendung
Primäre Adapter bzw. Driving Adapters bilden die technische Anbindung an die Anwendung. Sie steuern den jeweiligen UseCase über Aufrufe entsprechender primärer Ports.
Ein REST-Controller als primärer Adapter übersetzt HTTP-Requests in Geschäftsoperationen:

Beispiel 2:
@RestController
@RequestMapping("/api/users")
class UserRegistrationController(
private val userRegistrationUseCase: UserRegistrationUseCase
) {
@PostMapping("/register")
fun registerUser(@RequestBody request: UserRegistrationRequest): ResponseEntity<*> {
try {
userRegistrationUseCase.registerUser(request.toDomain())
return ResponseEntity.ok()
} catch(Exception e) {
return ResponseEntity.badRequest().body(createErrorResponse(e))
}
}
}
Sekundäre Adapter – Die Anbindung der Infrastruktur
Hier kommt das Dependency Inversion Principle zum Einsatz: Die Geschäftslogik definiert die Schnittstelle über sekundäre Ports, die Infrastruktur implementiert sie durch einen sekundären Adapter. Dadurch bleibt die Geschäftslogik unabhängig von konkreten Technologien und kann auch dann bestehen, wenn sich externe Systeme oder Frameworks ändern.

Beispiel 3:
class JpaUserRepository(
private val springDataUserRepository: SpringDataUserRepository
) : UserRepository {
override fun add(user: User): User {
val entity = mapToEntity(user)
val savedEntity = springDataUserRepository.save(entity)
return savedEntity.toDomain()
}
override fun existsByEmail(email: String): Boolean {
return springDataUserRepository.existsByEmail(email)
}
}
Mapping zwischen Schichten – Eine bewusste Entscheidung
Ein zentraler Aspekt der hexagonalen Architektur ist das Mapping zwischen Domain-Objekten und Infrastruktur-Objekten.
- Domain-Objekte repräsentieren die Fachlogik – also Begriffe und Regeln aus der Anwendungsdomäne, z. B. Order oder Invoice. Sie sind frei von technischen Details.
- Infrastruktur-Objekte (oder Adapter-Modelle) sind dagegen an Frameworks oder externe Systeme angepasst, z. B. Datenbank-Entities oder DTOs für eine REST-API.
Auch wenn dieses Mapping zunächst nach zusätzlichem Aufwand aussieht, bietet es entscheidende Vorteile:
- Technische Details bleiben außen: Annotationen für z. B. Validation-API, JSON-Serialisierung, JPA und andere Framework-spezifische Details sind nur in den Adapter-Modellen vorhanden und beeinflussen nicht die Modelle der Geschäftslogik.
- Flexibilität bei Technologiewechseln: Ein Wechsel von z. B. REST zu Message-Bus erfordert nur Änderungen im Adapter.
- Klare Datenkontrakte: Jede Schicht hat ihre eigenen, optimierten Datenstrukturen.
Allerdings hat dieses Vorgehen auch seinen Preis: Für jede Schnittstelle müssen Mapping-Logik und Datenkonvertierungen gepflegt werden. In kleineren Projekten kann der Overhead schnell unverhältnismäßig wirken – in komplexeren Anwendungen überwiegen jedoch meist die Vorteile.
Beispiel 4:
data class UserRegistrationRequest(
@Size(min = 3) val name: String,
@Email val email: String
) {
fun toDomain() = User.create(name = name, email = email)
}
@Entity
@Table(name = "users")
data class UserEntity(
@Id val id: String,
@Column(unique = true) val email: String,
val name: String,
@Column(name = "created_at") val createdAt: LocalDateTime
) {
fun toDomain() = User(id = id, name = name, email = email)
}
Testing
Ein weiterer Vorteil zeigt sich beim Testen: Durch die klare Abgrenzung lassen sich fachliche Tests unabhängig von der Infrastruktur leicht realisieren.
Die hexagonale Architektur macht isoliertes Testen elegant möglich. Die Geschäftslogik kann vollständig ohne externe Abhängigkeiten getestet werden, da nur die Port-Schnittstellen durch Mocks ersetzt werden müssen.
Beispiel 5:
class UserRegistrationServiceTest {
private val mockUserRepository = mockk<UserRepository>()
private val mockUserNotificationService = mockk<UserNotificationService>()
private val userRegistrationService = UserRegistrationService(
mockUserRepository,
mockUserNotificationService
)
@Test
fun `sollte Benutzer erfolgreich registrieren`() {
// Arrange
val user = User (email = "test@example.com", name = "Test User")
every { mockUserRepository.existsByEmail(user.email) } returns false
every { mockUserRepository.save(any()) } returnsArgument 0
every { mockUserNotificationService.sendWelcomeMessage(any()) } just Runs
// Act
userRegistrationService.registerUser(user)
// Assert
verify { mockUserRepository.save(any()) }
verify { mockEmailService.sendWelcomeMessage(user) }
}
}
Vorteile für nachhaltige Softwareentwicklung
Die hexagonale Architektur bietet konkrete Vorteile für die langfristige Wartbarkeit von Software:
- Flexibilität bei Technologie-Entscheidungen: Frameworks oder externe APIs können ausgetauscht werden, ohne die Geschäftslogik anzufassen.
- Bessere Testbarkeit: Geschäftslogik ist vollständig isoliert testbar.
- Klarere Strukturen: Fachliche und technische Aspekte sind klar getrennt, insbesondere ist die Geschäftslogik leichter fassbar und damit wartbarer.
- Parallelentwicklung: Teams können parallel an Adaptern und Geschäftslogik arbeiten, sobald die Schnittstellen durch Ports festgelegt sind.
- Reduzierte technische Schulden: Upgrades und Refactorings werden weniger riskant.
Wann lohnt sich der Einsatz?
Trotz dieser Vorteile ist die hexagonale Architektur nicht für jedes Projekt die richtige Wahl. Sie eignet sich besonders für:
- Komplexe Geschäftsanwendungen mit erwarteter Lebensdauer von mehreren Jahren.
- Projekte mit sich ändernden Technologieanforderungen.
- Anwendungen mit komplexer Fachlogik, die isoliert testbar sein muss.
Der zusätzliche Aufwand durch Mappings zwischen Domain- und Infrastruktur-Objekten ist dabei der größte Nachteil. Für einfache Anwendungen oder Prototypen kann dieser Overhead schnell überdimensioniert sein.
Fazit
Die hexagonale Architektur bringt Ordnung in komplexe Systeme: Fachlichkeit im Kern, Technik sauber entkoppelt. Anwendungen werden dadurch langlebig, testbar und flexibel – genau die Eigenschaften, die in zukunftssicheren Softwareprojekten entscheidend sind.
Natürlich hat der Ansatz seinen Preis. Das zusätzliche Mapping erzeugt Aufwand, der in kleinen oder kurzfristigen Projekten schnell überdimensioniert wirkt. Doch in komplexen Geschäftsanwendungen zahlt sich diese Investition aus: Sie verhindert technische Abhängigkeiten, reduziert Risiken bei Technologiewechseln und schafft die Basis für nachhaltige Weiterentwicklung.
Damit ist die hexagonale Architektur kein Allheilmittel, sondern ein strategisches Werkzeug. Wer langfristig stabile Systeme entwickeln will, profitiert von der klaren Trennung zwischen Fachlichkeit und Technik. Besonders spannend wird es, wenn dieser Ansatz mit weiteren Konzepten wie Domain Driven Design (DDD) kombiniert wird – denn beide verfolgen das Ziel, Fachlichkeit konsequent ins Zentrum der Software zu stellen. Mehr zu DDD selbst gibt es in einem unserer früheren Blogbeiträge.