Softwarearchitektur ist wie ein Bauplan beim Hausbau: Ohne soliden Plan wirkt am Anfang vielleicht alles noch machbar, aber spätestens, wenn mehr Stockwerke dazukommen sollen oder jemand eine neue Tür einbauen will, wird’s kompliziert. Leitungen verlaufen kreuz und quer, Räume sind unlogisch angeordnet – und plötzlich muss man viel mehr umbauen, als nötig wäre. In der Software ist das genauso: Eine gute Architektur sorgt dafür, dass alles sinnvoll aufgebaut ist, gut zusammenpasst und sich später einfach erweitern oder ändern lässt, ohne dass man ständig Wände einreißen muss.
Aber ein Plan ist nur so gut wie seine Umsetzung – und genau da wird’s oft schwierig. In Projekten stehen wir immer wieder vor der Herausforderung, Architekturvorgaben konsequent einzuhalten. Besonders in großen, langjährigen Projekten mit einer gewachsenen Codebasis ist die Komplexität oft hoch und dadurch schwer beherrschbar. Das macht die Entwicklung neuer Funktionalitäten, egal ob klassisch oder mit Coding Assistant, oder auch die Einarbeitung neuer Teammitglieder immer herausfordernder. Missverstandene oder nicht bekannte Architekturvorgaben können dann schnell zu Problemen führen und plötzlich sind Abhängigkeiten oder zusätzliche Aufwände da, die da nie hätten sein sollen. Dabei sind klare Architekturvorgaben kein Selbstzweck: sie helfen uns, sauberen, wartbaren Code zu schreiben und technische Schulden zu vermeiden. Eine Architektur-Dokumentation für ein Projekt ist hilfreich, stellt aber keine korrekte Umsetzung sicher. Wie auch beim Testen von Funktionalität zeigt sich beim manuellen Überprüfen der Architektur, dass dies aufwändig und auf Dauer nicht immer zuverlässig ist – Automatisierung ist im Bereich der fachlichen Tests der Schlüssel, um Qualität nachhaltig sicherzustellen. Warum also nicht auch bei der Architektur?
Hier setzt ArchUnit an – eine Bibliothek, mit der Architekturregeln in Code festgehalten werden können, um diese in Java-Projekten automatisiert zu prüfen. Doch wie genau kann ArchUnit helfen, typische Probleme zu vermeiden? Wir zeigen dies anhand mehrerer Beispiele aus der Praxis.
ArchUnit in a Nutshell
ArchUnit wird einfach via Maven oder Gradle eingebunden und ermöglicht es, Architekturregeln in Form von Unit-Tests zu definieren. Dabei nutzt es eine Fluent API, um Regeln intuitiv und gut lesbar zu schreiben. Die Regeln können direkt in bestehende Testsuiten integriert und automatisch bei jedem Testlauf überprüft werden. So lassen sich Verstöße gegen Architekturvorgaben frühzeitig erkennen und beheben.

Bild: https://www.archunit.org/getting-started#let-the-api-guide-you
Beispiel: Abhängigkeiten zwischen Paketen managen
Bei einer Schichtenarchitektur sind Abhängigkeiten zwischen den Schichten eingeschränkt, es darf nur auf Klassen in der eigenen oder direkt darunter liegenden Schicht zugegriffen werden. In einer typischen 3-Schicht-Architektur mit Controller-, Service- und Repository-Schicht dürfte dann zum Beispiel ein Controller keine direkten Zugriffe auf Repository-Klassen haben sowie Services nur von anderen Services oder Controllern eingebunden werden. Anstatt regelmäßig manuelle Reviews durchzuführen, um diese Vorgaben einzuhalten, können automatisierte ArchUnit-Regeln dafür wie folgt aussehen:
@ArchTest public static final ArchRule controller_should_not_access_repositories_directly = noClasses().that().resideInAPackage("..controller..").should() .accessClassesThat().resideInAPackage("..repository..");
@ArchTest public static final ArchRule services_should_only_be_accessed_by_controller_and_service = classes().that().resideInAPackage("..service..").should()
.onlyBeAccessed().byAnyPackage("..controller..", "..service..");
Abhängig vom Paket, in dem sich eine Klasse befindet, wird hier festgelegt, aus welchen anderen Paketen ein Zugriff stattfinden darf. Statt einem voll qualifiziertem Paketnamen wird hier mittels Wildcards lediglich auf den Paketnamen an beliebiger Stelle im Projekt gematcht. Die erste Regel fordert also, dass keine Klassen, die innerhalb eines Pakets „controller“ enthalten sind, Klassen innerhalb eines Pakets „repository“ aufrufen dürfen.
Diese Regeln lassen sich wie andere Unit-Tests ausführen. Einmal definiert, lässt sich so die Einhaltung von Architekturvorgaben bei jeder zukünftigen Ausführung der Tests automatisiert und ohne zusätzlichen Aufwand sicherstellen. Und im Fall einer Regelverletzung wird ein Fehler ausgegeben – ähnlich sprechend wie die Regeldefinition, zum Beispiel:
java.lang.AssertionError: Architecture Violation -
Rule 'no classes that reside in a package '..controller..' should
access classes that reside in a package '..repository..'' was violated (1 times):
Constructor <de.isento.arch.demo.controller.MyDemoController.<init>()> calls
constructor <de.isento.arch.demo.repository.MyDemoRepository.<init>()> in (MyDemoController.java:15)
Migrationen sind ein weiteres Themenfeld, bei dem ArchUnit Unterstützung leisten kann. So lässt sich etwa sicherstellen, dass neuer Code keine unerwarteten Abhängigkeiten zum Bestandscode aufbaut. Eine Regel hierfür könnte, ähnlich wie die Regeln zur Schichtenarchitektur, festlegen, dass Pakete mit neuer Implementierung nicht auf abzulösende Pakete zugreifen dürfen.
Will man nicht nur die Abhängigkeiten einzelner Klassen oder Pakete untereinander, sondern weitreichender die Paketstruktur eines Projektes definieren, ist dies mit einem Abgleich gegen PlantUML-Diagramme möglich. Aus einer reinen Visualisierung einer Architektur in UML wird so ein ausführbarer Test auf die Einhaltung dieser. Ein Beispiel für die Einbindung eines Diagramms sieht wie folgt aus:
static final URL myArchDiagram = MyDemoArchTest.class.getClassLoader().getResource("myArchDiagram.puml");
@ArchTest
static final ArchRule uml_diagram_should_be_respected =
classes().should(
adhereToPlantUmlDiagram(myArchDiagram, consideringOnlyDependenciesInAnyPackage("de.isento..")));
Beispiel: Einhaltung von Querschnittskonzepten
Ein weiterer Anwendungsfall, für den sich ArchUnit eignet, ist die Sicherstellung der Einhaltung von Querschnittskonzepten. Betrachten wir beispielsweise eine selbstgeschriebene Logging-Bibliothek, welche Logmessages um Kontext (etwa aus dem Request oder Methodenparametern) anreichert. Es soll in diesem Fall hier im Beispiel sichergestellt werden, dass bei jedem Schnittstellenaufruf einer REST-API im Controller – und auch genau nur an dieser Stelle in der Anwendung – die Logging-Bibliothek mittels Annotationen entsprechend eingebunden ist. Ohne ein Tool wie ArchUnit besteht die Gefahr, dass versehentlich die Einbindung ganz vergessen oder die Bibliothek an falscher Stelle oder mehrfach eingebunden wird.
Mit ArchUnit lassen sich gezielt Regeln definieren, die genau dies verhindern. Dies könnte in einem Spring Boot Projekt wie folgt aussehen:
@ArchTest
public static final ArchRule all_rest_endpoints_must_have_logging_annotation =
methods().that().areMetaAnnotatedWith(RequestMapping.class)
.should().beAnnotatedWith(MyDemoLoggingAnnotation.class);
@ArchTest
public static final ArchRule only_rest_endpoints_must_have_logging_annotation =
methods().that().areAnnotatedWith(MyDemoLoggingAnnotation.class)
.should().beDeclaredInClassesThat().areMetaAnnotatedWith(Controller.class);
Diese Regeln stellen sicher, dass das Logging konsistent eingebunden wird und auch bei zukünftigen Änderungen keine Klasse vergessen wird.
Bestehende Codebasen verbessern mit dem Freeze-Modul
Selbst wenn bestehender Code noch nicht allen gewünschten Regeln entspricht, ist ArchUnit hilfreich. Mit FreezingArchRules lassen sich bestehende Regelverletzungen zunächst festhalten und „einfrieren“. Bei Testläufen danach werden diese Regelverstöße bis zur Behebung toleriert, und lediglich neue Verstöße führen zu einem Fehlschlag des Testlaufs. Damit wird es möglich, dass mit geringem Aufwand neue Regeln eingeführt werden können, ohne dass sofort der gesamte Altbestand bereinigt werden muss. So können Architekturregeln auch in gewachsenen Systemen schrittweise etabliert werden, ohne den Entwicklungsfluss zu stören.
Ein Beispiel für eine „eingefrorene“ Regel sieht folgendermaßen aus:
@AnalyzeClasses(packages = "de.isento")
public class MyArchitectureTest {
@ArchTest
public static final FreezingArchRule frozen_rule =
FreezingArchRule.freeze(
noClasses().that().resideInAPackage("..controller..").should()
.accessClassesThat().resideInAPackage("..repository..");
);
}
Fazit: ArchUnit als Bestandteil einer nachhaltigen Softwarearchitektur
Die beschriebenen Anwendungsfälle vermitteln einen ersten Eindruck, wie ArchUnit als automatisierter Wächter der Softwarearchitektur fungiert. Durch frühzeitige Erkennung von Regelverstößen lassen sich langfristig Wartbarkeit und Qualität der Codebasis sicherstellen.
In unseren Projekten setzen wir ArchUnit gezielt ein, um uns und unsere Kunden bei der Einhaltung von Architekturprinzipien zu unterstützen. Mit der richtigen Strategie und Automatisierung gelingt so die nachhaltige Entwicklung robuster Softwarelösungen.

Jona
Jona ist Head of Software Engineering bei isento





