In Projekten mit Hunderttausenden von Codezeilen verliert man leicht den Überblick über Code, Architektur und Qualität. Sind wir noch auf Kurs? Blockieren wir uns selbst durch interne Abhängigkeiten? Oder stecken wir bereits fest? Software ist immateriell; wir können nicht sehen, in welchem Zustand sie wirklich ist.
Um das herauszufinden, müssen wir systematisch prüfen, ob das, was die Entwickler im Sinn hatten, auch mit dem tatsächlichen Code übereinstimmt. Spoiler: Das tut es leider nicht. Die Gründe dafür sind, dass Software sich schneller verändert, als man hinterherkommt, dass jeder Entwickler seine eigenen Vorstellungen hat und zu guter Letzt, dass keiner das Gesamtbild hat.
Table of Contents
Den Abgleich von Erwartung und Realität nennen wir Code Mapping. Ohne diese Methode kann man zwar ein Ziel vor Augen haben, weiß aber nicht, ob der eingeschlagene Weg überhaupt dorthin führt.
Stell dir vor, du navigierst nur mit einem Kompass. Du kannst dich entscheiden, nach Osten zu gehen, aber ob du am Ende in New York, Paris oder gar mitten im Ozean landest, hängt davon ab, wo genau dein Startpunkt ist. Wenn du deinen Standort nicht kennst, kannst du überall ankommen, nur nicht da, wo du eigentlich hinwolltest.
Ähnlich ist es beim Refactoring: Du kannst zwei Monate lang umstrukturieren, aber ob dich das wirklich weiterbringt, hängt davon ab, wo du aktuell stehst. Nicht jeder Teil einer Software muss hochflexibel sein, und manchmal gibt es wichtigere Themen, wie etwa Sicherheit.
Um ein Ziel zu erreichen, brauchst du also beides: eine Richtung und deinen aktuellen Standort. Genau deshalb ist Code Mapping entscheidend, wenn man seine Architektur wirklich meistern will.
Architektur lässt sich definieren als:
„Die Softwarearchitektur eines Systems ist die Menge der Strukturen, die wir brauchen, um über das System zu schlussfolgern. Diese umfassen Software-Elemente, deren Beziehungen und die Eigenschaften beider.“
„Software Architecture in Practice“ vom Software Engineering Institute
Mit Mapping lassen sich die echten Strukturen im Code sichtbar machen und wie sie miteinander, mit ihrer Umgebung und sogar mit den Entwicklern verknüpft sind. Das ist ein starkes Werkzeug, aber längst nicht das einzige, das Architekten zur Verfügung steht. Auch neue Ansätze wie LASR oder die Residuality Theory sind ebenfalls einen Blick wert.
Doch zurück zum Mapping. Es ermöglicht uns:
- Änderungen gezielt zu steuern
- Kosten- und Zeitabschätzungen zu verbessern
- eine Grundlage für Einarbeitung und Trainings zu schaffen
- die Kommunikation zwischen Stakeholdern zu erleichtern
Die folgenden Kapitel zeigen, was Code Mapping genau ist und wie man es in der Praxis anwendet.
Über das Mappen
Wir können unsere Architektur besser verstehen, wenn wir regelmäßig prüfen, ob unsere Vorstellungen mit der Realität übereinstimmen, also mit dem, was tatsächlich im Code steht.
Code Mapping ist etwas anderes, als einfach Kästchen und Linien auf ein Blatt zu zeichnen. Auch das kann sinnvoll sein, weil wir damit unser Verständnis der Architektur festhalten. Solche Skizzen zeigen unsere Erwartungen, die wir anschließend bestätigen oder widerlegen können.
Richtig spannend wird es aber erst, wenn wir uns die Realität ansehen. Dafür brauchen wir Fakten, also den Quellcode oder die Daten aus Git, aus denen wir Informationen ableiten und visualisieren. Wie genau das funktioniert, hängt davon ab, welche Daten verfügbar sind, wie leistungsfähig die Tools sind und welche Aspekte wir abbilden wollen.
Sobald die Werkzeuge eingerichtet sind, beginnt der iterative Designprozess, der sich am PDCA Zyklus (Plan–Do–Check–Act) orientiert:

- Wir treffen architektonische Entscheidungen.
- Wir setzen sie um.
- Wir mappen die Ergebnisse.
- Wir passen unser Verständnis und, wenn nötig, auch unsere Architektur an.
Es gibt viele Dinge, die sich in einer Codebasis abbilden lassen. Ein guter Einstieg sind die großen Strukturen des Codes.
Code-Strukturen mappen
Der erste Schritt besteht darin, die Struktur der Codebasis zu kartieren.
Ich bevorzuge dafür das Open-Source-Tool CodeCharta , weil es kostenlos, aktiv gepflegt und seit 2017 kontinuierlich weiterentwickelt wird.
CodeCharta visualisiert Metriken einer Codebasis als 3D-Treemap. Erfahrene Entwickler kennen diesen Ansatz vielleicht von CodeCity (https://wettel.github.io/codecity.html), dessen letzte Version 2009 erschien. Ich erhebe hier keinen Anspruch auf Originalität – die „Stadt“-Metapher hat sich einfach als äußerst wirkungsvolle Visualisierung für komplexe Codebasen bewährt. Der Mehrwert von CodeCharta liegt vor allem darin, dass es leicht zugänglich und gut gepflegt ist.
Wichtig ist: Code Mapping ist eine universelle Methode, die nicht an ein bestimmtes Tool gebunden ist. Man sollte jedoch immer Werkzeuge verwenden, um sicherzustellen, dass das Abbild auf Fakten basiert und nicht auf Bauchgefühl. Ich nutze an dieser Stelle CodeCharta, weil es frei verfügbar ist und ich an dessen Entwicklung beteiligt war. Wenn du andere Tools besser kennst, nutze gern diese stattdessen. Am Ende des Artikels findest du eine Übersicht.
Schauen wir uns nun ein Beispiel an: Discourse. Jede Datei wird dabei als Gebäude dargestellt. Die Größe des Gebäudes (✥) entspricht den „Real Lines of Code“ (RLoC) – also den realen Codezeilen, die man lesen muss, um den Code zu verstehen. Kommentare und Leerzeilen werden daher nicht gezählt.

In der Visualisierung erkennt man sofort, dass Discourse im Wesentlichen aus drei technischen Clustern besteht: app (259 k), plugins (254 k) und spec (240 k). Das zeigt, dass Discourse darauf ausgelegt ist, sich über Plugins anpassen zu lassen. Außerdem wird deutlich, dass Tests einen hohen Stellenwert haben: app und spec stehen nahezu im Verhältnis 1:1, und jedes Plugin bringt eigene Tests mit – eindeutig sichtbar, wenn man *spec* hervorhebt.
Was uns diese Visualisierung nicht zeigt, ist, wofür Discourse eigentlich da ist.
Der Ordner app, vermutlich der Kern der Anwendung, ist technisch strukturiert – nach models, services und controller. Wahrscheinlich, weil Ruby-on-Rails-Anwendungen idiomatisch so aufgebaut sind.
Dieses Muster nennt man Package-by-Layer: Es gruppiert Code nach technischen Schichten, die oft das Framework vorgibt, nicht nach inhaltlicher Beziehung. Man erkennt also das Framework – Rails –, aber nichts über die Domäne, die die Software abbildet.
Dieses Muster ist problematisch, weil es schwer macht, auf einen Blick zu erkennen, was zusammengehört. Das erschwert wiederum die Einarbeitung neuer Entwickler (der erste Eindruck ist meist die Verzeichnisstruktur) und führt dazu, dass sich die Implementierung einzelner Features über mehrere Codebereiche verteilt.
Im Gegensatz dazu sind die Plugins anders strukturiert, sie folgen eher dem Muster Package-by-Feature: Alle Schichten eines Features (Controller, Models usw.) befinden sich am selben Ort (vgl. Martin Fowler’s Bliki oder auch dieser post von Simon Brown). Positive Beispiele sind Plugins wie chat, calendar oder poll deren Namen sofort etwas über den Fachkontext verraten, im Gegensatz zu abstrakten Schichtenbegriffen. Auch die Tests profitieren davon: Jeder Test im Ordner chat testet eindeutig das Feature chat – ein klarer, logischer Zusammenhang.
Das erscheint mir um einiges verständlicher, denn wir folgen damit einem Prinzip aus der Neurowissenschaft (der Hebb’schen Theorie): „What fires together, wires together.“
Wenn man dieses Prinzip konsequent umsetzt, erhält man eine nahezu lehrbuchhafte Code-Struktur wie in folgendem Beispielprojekt:

Wenn man in der Codebasis Begriffe wie catalogue, lending oder patron sieht, weiß man sofort, worum es inhaltlich geht.
Natürlich ist es nicht ganz fair, dieses Beispielprojekt mit rund 3.000 Zeilen Code mit einem riesigen System wie Discourse zu vergleichen. Ich habe das aus zwei Gründen gemacht. Zum einen war es mir wichtig, zu zeigen, wie der fachliche Zweck direkt über die Struktur kommuniziert werden kann. Zum anderen gibt es keine großen OSS-Projekte, die Package-by-Feature nutzen und diesen dann auch fachliche Namen geben. Die großen Open-Source-Projekte unterstützen “nur” geschäftliche Prozesse, sind aber nicht selbst Teil des Kerngeschäfts. Daher tragen die Features auch technische Namen wie validation und nicht fachlich sprechende Namen wie patron.
Ein OSS-Projekt mit Package-By-Feature ist zum Beispiel das PHP-Webframework Laravel:

Hier erkennt man sofort, welche Komponenten Laravel seinen Nutzern bereitstellt. Die Logik für etwa validation oder database ist klar an einem Ort gebündelt, nicht über viele Ordner verteilt. Hat man sich einmal an die technischen Namen gewöhnt, wird schnell klar, worum es in Laravel geht.
Ich empfehle dir, Package-by-Feature (oder die von Simon Brown vorgeschlagene Variante Package-by-Component) einmal auszuprobieren. Es kann dein erster „Adjust“-Schritt im iterativen Code-Mapping-Zyklus sein. Nach dem Adjust folgt dann oft der nächste Aha-Moment. Denn danach erkennt man plötzlich, wie viel Code pro Feature geschrieben wurde – und das führt zur nächsten Frage: Wird das alles überhaupt genutzt?
Wenn sich zeigt, dass nur ein Bruchteil einer Funktion tatsächlich von Nutzern verwendet wird (was man z. B. durch Feature Usage Mapping herausfinden kann), kann man gezielt überflüssigen Code löschen.
Code-Komplexität mappen
Als Nächstes betrachten wir das Spring Framework:

Diesmal legen wir die zyklomatische Komplexität auf die Höhe (↨) und die Farbe (🖌) der Gebäude. Diese Metrik misst, wie viele Entscheidungspfade es im Code gibt – also, wie oft dort Verzweigungen wie if, case, for, catch usw. auftreten. Je mehr Verzweigungen, desto mehr Entscheidungen müssen beim Durchlaufen des Codes getroffen werden.
In der Visualisierung werden Dateien mit mehr als 100 Verzweigungen gelb und mit über 200 rot eingefärbt. Ich persönlich arbeite gern mit niedrigeren Schwellenwerten, aber diese Werte liefern bereits einen guten Überblick, wo im Spring Framework die Hauptkomplexität liegt.
Deutlich wird: spring-core und spring-beans sind komplex und schwer zu durchdringen, da ihre Dateien groß und stark verzweigt sind. Aber auch der WhatWgUrlParser in spring-web ist bemerkenswert: Er umfasst rund 2.000 RLoC und eine zyklomatische Komplexität von 840.
Der Grund: Er muss nicht standardkonforme URLs (abweichend von RFC 3986) so interpretieren, wie Browser es tun, und das erfordert eben viel Code, um fehlertolerant zu sein.
Der Begriff „Cyclomatic Complexity“ stammt von Thomas McCabe aus dem Jahr 1976 – also lange bevor Java (1995) oder C++ (1985) existierten. Die Metrik entstammt dem Bereich der „Computational Complexity Theory“, also der rechnerischen Komplexität von Algorithmen.
Allerdings hat der Begriff _Komplexität_ in der Komplexitätsforschung eine völlig andere Bedeutung:
Hier beschreibt er emergentes Verhalten, also Systeme, in denen neue Eigenschaften entstehen, die nicht aus den Einzelteilen erklärbar sind. Oder wie Barry O’Reilly, der Erfinder der Residuality Theory, erklärt:
„Komplexität bedeutet das Vorhandensein emergenter Verhaltensweisen und Eigenschaften in einem System.“
Code hingegen zeigt kein emergentes Verhalten. Er tut exakt, was wir ihm vorschreiben. Daher ist Software nicht komplex, sondern kompliziert. Genau genommen müsste die Metrik also „Cyclomatic Complicatedness“ heißen.
In diesem Artikel bleiben wir beim Begriff _Komplexität_, um konsistent zum Namen der Metrik zu bleiben.
Die Komplexitätskarte allein sollte allerdings nicht zu einem “Adjust” führen. Die Ansicht ist nützlich (und oft aufschlussreich), um zu sehen, wo die Komplexität sitzt –
aber wirklich leitend wird sie erst, wenn man sie mit anderen Metriken kombiniert.
Bevor wir das tun, folgt jedoch ein Kapitel der Warnung.
Über Metriken
„When a metric becomes a target, it ceases to be a good metric.“
Freie Interpretation von Goodhearts Law
Goodharts Gesetz bringt das Problem mit Metriken perfekt auf den Punkt:
Sobald eine Metrik zum Ziel wird, taugt sie nicht mehr als Messgröße.
Ein Beispiel:
Das Ziel lautet: „die Komplexität dieser 100 Klassen innerhalb von vier Wochen auf unter 30 zu senken“. Das lässt sich leicht erreichen, etwa durch exzessives „Extract Class“-Refactoring. Ob der Code danach verständlicher ist, spielt keine Rolle. Wenn man nach der zyklomatischen Komplexität pro Klasse bewertet oder bezahlt wird, sorgt man schlicht dafür, dass sie niedrig ist, koste es, was es wolle. Ziel erreicht, Patient tot.
Trotzdem sind Metriken wertvoll um ein Gespräch zu beginnen, besonders wenn man sie die Kombination mehrerer Metriken betrachtet:
„Warum ist diese hochkomplexe Klasse so schlecht getestet? Müssen wir da etwas unternehmen? Gefährdet die geringe Testabdeckung vielleicht zukünftige Features?“
Metriken helfen uns, uns durch große Codebasen zu manövrieren. Sie zeigen, wo wir hinschauen sollten. Aber sie liefern keine Antworten.
Wissenssilos mappen
Wann wird komplexer Code wirklich zum Problem? Spätestens dann, wenn nur eine Person ihn versteht.
Ein Beispiel aus einem Kundenprojekt: Wir haben die Anzahl der Autoren einer Datei über die Farbe (🖌) visualisiert – diese Information stammt direkt aus dem Git-Log.
Eine Datei wird zum Wissenssilo, wenn nur wenige Personen sie kennen. In der folgenden Darstellung ist jede Datei mit nur einem Autor rot, mit zwei Autoren gelb und mit drei oder mehr Autoren weiß markiert.

Die Librarian-Klasse war in diesem Fall hochkomplex, hatte aber nur einen Autor und war zentral für ein geschäftskritisches Feature. Ein klassischer Fall, in dem man das Wissen verteilen sollte – z. B. durch Pair- oder Ensemble-Programming. Erst recht, wenn die betreffende Person bald das Team verlässt.
Aber: Nicht jedes Wissenssilo ist automatisch ein Risiko. Es hängt vom Kontext ab. Zum Beispiel:
- Das Team arbeitet regelmäßig gemeinsam am Code, nutzt aber Git Trailer wie
Co-Authored-by:nicht. - Die Git-Metrik ist verfälscht, weil bsp. Pull Requests immer vom gleichen Maintainer gesquasht werden (was generell keine gute Praxis ist).
- Die Datei wird äußerst selten geändert.
Es lohnt sich daher, genauer zu betrachten, wie häufig eine Datei wirklich geändert wird.
Häufig geänderten Code mappen
Auch sehr komplexer Code ist nicht automatisch problematisch. Wenn er einfach funktioniert und stabil bleibt, besteht kein Handlungsbedarf. Aber was, wenn er ständig verändert wird, wenn es viele Einfügungen {+} oder Löschungen {-} gibt? Komplexer Code ist deutlich schwerer zu verstehen als einfacher. Änderungen dauern länger, sind fehleranfälliger und dadurch teurer. Auch solche Bereiche lassen sich mappen: Im folgenden Beispiel wird das Verhältnis von Einfügungen zu Löschungen – auch „Churn“ genannt – über die Farbe dargestellt.

Der ModuleService im linken oberen Bereich springt sofort ins Auge: Er ist groß, komplex und ständig in Bewegung. Dazu kommt: Sein Name ist nichtssagend.
Ein idealer Kandidat für Refactoring, bevor dort das nächste Feature eingebaut wird.
Aber auch hier gilt: Nicht vorschnell handeln. Vielleicht gibt es Kopplungen zu anderen Modulen, die all diese Änderungen erklären.
Change Coupling mappen
Diesmal betrachten wir dasselbe Kundenprojekt, stellen aber die Anzahl der Autoren über die Farbe dar. Eine hohe Zahl an Autoren weist auf einen Koordinationsflaschenhals hin: Viele Personen müssen dieselbe Datei ändern, weil sie in mehreren Kontexten verwendet wird. Zusätzlich lässt sich auch die sogenannte Change Coupling (Änderungskopplung) visualisieren: Wenn zwei oder mehr Dateien häufig gemeinsam committet werden, gelten sie als gekoppelt. Um die eine zu ändern, muss auch die andere angepasst werden; das lässt sich direkt in der Git-Historie erkennen. Die daraus entstehende Karte ist unten zu sehen.

Wir sehen deutlich, dass der ModuleService in Bezug auf Autoren und Change Coupling unproblematisch ist. Die Komplexität ist hoch, ebenso der Churn, aber es gibt keinen Koordinationsengpass. Beim RentService ist das anders: Er wird gemeinsam mit vielen anderen Dateien committet, obwohl er diese Dateien nicht importiert und umgekehrt ebenso wenig.
Das deutet auf eine versteckte Kopplung zwischen RentService und diesen Dateien hin. Solche Kopplungen können viele Formen annehmen. In sogenanntem stringly typed Code (also Code, in dem Typen durch Strings statt durch explizite Modelle dargestellt werden) entsteht sie zum Beispiel, weil an mehreren Stellen mit denselben String-Konstanten gearbeitet wird. Es gibt aber noch viele weitere Varianten solcher versteckten Abhängigkeiten (siehe auch Connascence). Diese Zusammenhänge sollten im Team genauer untersucht werden.
Die Änderung der Kopplung wurde erstmals von Adam Tornhill in seinem Buch [Your Code as a Crime Scene](https://adamtornhill.com/articles/crimescene/codeascrimescene.htm) unter dem Namen „Temporal Coupling” vorgeschlagen. Sie ist auch in dem Tool [CodeScene](https://codescene.com/) verfügbar, das mehr als einen Blick wert ist.
Abhängigkeiten mappen
Als Nächstes betrachten wir die expliziten Kopplungen im Code, also die tatsächlichen Abhängigkeiten zwischen Dateien.
Import-Anweisungen erzeugen explizite Abhängigkeiten zwischen Dateien. Mit Tools wie SonarGraph lässt sich der kompilierte Bytecode analysieren und die Abhängigkeiten als Graph visualisieren. In diesem Artikel wird allerdings ein alternatives (zum Zeitpunkt des Schreibens noch nicht quelloffenes) Tool verwendet, das den Quellcode direkt parst – also ohne Kompilierungsschritt.
Wenn wir Abhängigkeiten untersuchen, interessieren uns besonders die Zyklen. Sie können in verschiedenen Formen auftreten.
Die offensichtlichste Form ist, wenn A direkt von B abhängt und B wiederum von A. Das kann schnell problematisch werden: Eine Änderung in A führt zu einer Änderung in B, die wiederum eine Anpassung in A nötig macht und so weiter.

Dann gibt es transitive Abhängigkeiten: A hängt von B ab, B von C und C wiederum von A. Auch hier kann eine Änderung an einem Element Auswirkungen auf alle anderen haben. Je länger diese Kette ist, desto stärker können sich Änderungen durch die gesamte Codebasis ziehen und unerwartete Nebeneffekte verursachen.

Schließlich gibt es architektonische Zyklen, bei denen ganze Features oder Schichten wechselseitig voneinander abhängen. Solche Probleme lassen sich sichtbar machen, indem man die Pakete zeilenweise von oben nach unten anordnet: Ein Verfahren, das ursprünglich von Structure 101 entwickelt wurde (heute Teil von Sonar).
In dieser Darstellung haben die Elemente in der obersten Reihe nur Abhängigkeiten zu den darunterliegenden, die Elemente in der untersten Reihe nur eingehende Abhängigkeiten, und Elemente innerhalb derselben Reihe keine gegenseitigen Abhängigkeiten.
In der Realität sieht das jedoch selten so sauber aus: In fast jeder Codebasis gibt es Zyklen. Um damit umzugehen, verwendet man eine einfache Heuristik: Das Element mit den meisten ausgehenden Abhängigkeiten wird höher platziert.
Das hat den Vorteil, dass wir alle nach unten gerichteten Pfeile ignorieren können und uns nur auf die roten Pfeile nach oben konzentrieren müssen. Dadurch wird das Bild deutlich übersichtlicher, und wir können uns gezielt auf die Zyklen konzentrieren, die wirklich relevant sind.
Im folgenden Beispiel (leicht inspiriert von einem bekannten Pen-&-Paper-Rollenspiel) kommt eine Variante der Onion Architecture von Jeffrey Palermo zum Einsatz. Normalerweise sollten alle Abhängigkeiten nach unten zur domain-Schicht zeigen – in diesem Fall hängt die domain jedoch auch von der übergeordneten application-Schicht ab.

Das Problem mit Zyklen ist, dass sie sich nur mit viel Aufwand beheben lassen, aber ganz leicht entstehen. Oft sogar unbeabsichtigt, selbst in erfahrenen Teams.

Die Grafik zeigt die Pakete, sortiert nach dem zuvor beschriebenen Algorithmus. Da die nach unten gerichteten Abhängigkeiten implizit sind, können wir sie ausblenden und so das Signal-Rausch-Verhältnis verbessern. Wir können uns auf die roten Pfeile konzentrieren, die nach oben zeigen. Diese nach oben gerichteten Abhängigkeiten, auch Feedback-Abhängigkeiten genannt, können leicht zu Knoten in unserem Gehirn führen.
Um
serviceszu verstehen, muss man zuerstuiverstehen, aber um das zu verstehen, muss man zuerstservicesverstehen, aber um das zu verstehen …
Dieser Deadlock tritt nicht nur im Gehirn auf. Er kann auch auftreten, wenn man etwas verändert, da dies zu Ripple-Effekten führen kann, bei denen eine Änderung in service zu einer Änderung in ui führt, was wiederum zu einer Änderung in service führt und so weiter. Normalerweise sollte die oberste Schicht sicher geändert werden können, ohne die unteren Schichten zu beeinträchtigen.
Der erste Schritt zur Beseitigung der roten Abhängigkeiten ist Package-by-Feature statt by-Layer. Dadurch wird klar, welche Features voneinander abhängig sind. Danach können Techniken zum Aufbrechen von Abhängigkeiten, wie z. B. vom Consumer definierte Schnittstellen, genutzt werden. Oft geht es darum, ein Element dort zu platzieren, wo es am meisten benötigt wird, und dann die Abhängigkeit anderer Features aufzubrechen oder deren Anfragen zu delegieren.
Ein Framework, das seit langem nach Features organisiert ist, ist Spring 🍃. Die Trennung ist recht stark, da jedes grobkörnige Feature in einem eigenen Gradle-Projekt 🧩 liegt. Auf diese Weise können die Projekte keine Zyklen zwischen sich aufweisen, da der Compiler dies nicht zulässt. Jedes dieser Projekte verfügt über eine eigene Pakethierarchie 📦.
🍃 spring-framework
├─🧩 spring-context
└─📁 src
├─📁 main/java
├─📦 org.springframework.cache
└─📦 org.springframework.context
├─📁 test/java
└─📦 org.springframework.context
└─📁 testFixtures/java
└─📦 org.springframework.context
├─🧩 spring-context-support
└─📁 src
├─📁 main/java
└─📦 org.springframework.cache
├─📁 test/java
└─📦 org.springframework.cache
└─📁 testFixtures/java
└─📦 org.springframework.contextsupport
├─🧩 etc.
Das Paket org.springframework.context erscheint sowohl im Produktionscode main als auch in test und testFixtures. Das ist normal. Ungewöhnlich ist, dass org.springframework.cache in mehreren Projekten genutzt wird. Dies wird wichtig, wenn wir die Abhängigkeiten des Frameworks mappen:

Wir erhalten Feedback, obwohl der Compiler dies nicht zulässt.
Ein Grund dafür ist, dass das von uns verwendete Tool mit Quellcode arbeitet, nicht mit den Build-Projekten, und die Dateien nach Paketen gruppiert. Dateien mit demselben Paketnamen, aber unterschiedlichen Build-Projekten, landen am selben Ort. Beispielsweise befindet sich org.springframework.cache in mehreren Gradle-Projekten. Dies ist als Split-Package-Problem bekannt und bei Verwendung des Java Platform Module System (JPMS) nicht zulässig. Wenn JPMS nicht genutzt wird, können sich Dateien im selben Paket, aber in unterschiedlichen JAR-Dateien, zur Laufzeit gegenseitig überschreiben oder auf die package private Mitglieder der jeweils anderen zugreifen, wodurch die Encapsulation möglicherweise aufgehoben wird. Soweit ich weiß, verwendet Spring JPMS nicht, da dessen Implementierung viele Einschränkungen und einen erheblichen Mehraufwand für das Team bedeutet.
Der wichtigere Grund für die Feedback-Abhängigkeiten ist jedoch, dass Testdateien Feedback-Abhängigkeiten erzeugen können, auch wenn der Produktionscode keine enthält. Beispielsweise hängen die Dateien in src/test/..cache von Dateien in src/testFixtures/..contextsupport ab, die wiederum von src/main/..cache abhängen.
Wenn wir die Ordner src/test und src/testFixtures entfernen und nur den Produktionscode in src/main belassen, erhalten wir ein viel übersichtlicheres Bild:

Wenn wir nur den Produktionscode in main betrachten, gibt es fast kein Feedback zwischen den Features auf dieser Zoom-Stufe. Wenn wir test und testFixtures mit einbeziehen, gibt es jedoch jede Menge Feedback. Dies führt zu einer interessanten Debatte. Einerseits kann src/main nicht von src/test abhängig sein (zumindest in Maven), sodass es bei einer Änderung des Codes nicht zu den Ripple-Effekten kommen kann. Andererseits können diese Testabhängigkeiten immer noch Knoten im Kopf verursachen.
Natürlich sollten wir nicht nur die Zyklen auf oberster Ebene betrachten, sondern auch, wie die Abhängigkeiten innerhalb der Pakete auf oberster Ebene aussehen. Wenn wir uns http genauer ansehen, was Teil von spring-web 🧩 ist, entsteht folgendes Bild:

In dieser Abbildung sind die Zykluspfeile aktiviert. Dabei handelt es sich um blaue Abwärtspfeile, die zusammen mit den roten Feedbackpfeilen einen Zyklus bilden. Beispielsweise implementiert DefaultBodyBuilder den BodyBuilder, der RequestEntity verwendet, welche wiederum DefaultBodyBuilder nutzt.
Um eine Architektur #flexibel zu halten, ist es von größter Bedeutung, all dieses Feedback zu managen. Ein guter erster Schritt ist es, sich einen Überblick zu verschaffen, indem man die bestehenden Rückmeldungen abbildet. Noch besser wäre es, wenn wir sie in Zukunft verhindern könnten, wobei uns das nächste Kapitel helfen kann.
Architekturverletzungen mappen
Die meisten Programmiersprachen sind recht eingeschränkt, wenn es darum geht, architektonische Regeln direkt abzubilden. In der Regel stehen nur Sichtbarkeitsmodifikatoren wie public, private oder protected zur Verfügung, die uns zur Design- oder Compile-Zeit schützen. Mit ArchUnit (oder jQAssistant ) lassen sich Java-Anwendungen um gezielte Architekturtests erweitern, die deutlich weitergehen. Für JavaScript- oder TypeScript-Projekte gibt es ähnliche Werkzeuge wie ts-arch, dependency-cruiser oder Sherrif.
ArchUnit bietet eine flüssige DSL, mit der sich Architekturregeln leicht beschreiben und verstehen lassen:
classes().that().resideInAPackage("..foo..")
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..");
Mit derselben Syntax lassen sich auch Schichtenregeln abbilden:
layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer();
Oder man prüft auf Paketzyklen:
slices().matching("com.myapp.(*)..").should().beFreeOfCycles();
Solche Regeln nachträglich in ein bestehendes Projekt einzuführen, ist allerdings nicht einfach. Alle gefundenen Verstöße auf einmal zu beheben, würde sehr lange dauern. Praktischer ist es, zunächst kritische Verstöße zu korrigieren, die bestehenden zu akzeptieren und gleichzeitig zukünftige zu verhindern. ArchUnit bietet dafür eine Funktion, mit der man bestehende Verstöße „einfrieren“ kann. Das heißt: Die aktuellen bleiben erlaubt, aber neue werden als Fehler erkannt.
freeze( // accept existing violations
noClasses().should().dependOnClassesThat().resideInAPackage("..service..")
);
Anschließend lassen sich diese Verstöße visualisieren, um gezielt zu erkennen, wo sich der Aufwand lohnt und wo man ansetzen sollte.

Feature-Verletzungen mappen
Die Darstellung einzelner Verstöße pro Datei ist für die meisten Stakeholder wahrscheinlich zu detailliert. Mein Kollege Andreas Blunk hatte dafür eine clevere Idee: Er hat die Verstöße pro Feature zusammengefasst und in dieser aggregierten Form dargestellt. Das funktioniert natürlich nur, wenn der Code nach Features strukturiert ist (Package-by-Feature). Das Ergebnis bietet Stakeholdern eine klare und verständliche Übersicht darüber, wie viel Arbeit noch vor ihnen liegt.

Andreas nutzte die aggregierte Ansicht außerdem, um den Fortschritt der Modernisierung sichtbar zu machen. Eine sogenannte Delta-Map zeigte, wo sein Team Architekturverstöße reduzieren konnte (grünes Dach) und wo neue Verstöße hinzugekommen oder akzeptiert wurden (rotes Dach). So konnten Stakeholder auf einen Blick erkennen, welche Bereiche bereits gut optimiert waren und wo noch Verbesserungspotenzial bestand. Besonders sinnvoll ist es, solche Karten in Reviews zu zeigen, wenn viele Änderungen „unter der Haube“ stattgefunden haben.

Fazit
Code Mapping ist der Prozess, bei dem Erwartungen mit der Realität abgeglichen werden. Davon profitieren nicht nur einzelne Entwickler, sondern ganze Teams. Eine gute Karte macht Code greifbar und verbessert die Kommunikation zwischen allen Beteiligten. Sie zeigt, welche Features eine Anwendung hat und wie viel Code (und damit Aufwand) in sie geflossen ist – eine wertvolle Grundlage etwa für Schulung oder Wissenstransfer.
Darüber hinaus lassen sich mit Hilfe der Metriken in der Karte Aufwand und Zeitpläne besser abschätzen. Man erkennt auf einen Blick, welche Bereiche #flexibel sind und welche Änderungen mit hohem Aufwand verbunden wären. Außerdem hilft das Mapping dabei, Veränderungen zu steuern: Es zeigt, wo Wissen geteilt werden sollte und wo sich Aufteilungen anbieten. In Kombination mit einer Feature-Roadmap lässt sich so gezielter planen und der Modernisierungsaufwand auf die Bereiche konzentrieren, die demnächst ohnehin verändert werden.
Wir alle nutzen Karten täglich – beim Autofahren, im öffentlichen Nahverkehr oder in Gebäuden. Orientierung wird deutlich leichter, wenn man weiß, wo man steht, und den Weg zum Ziel sehen kann. Genauso bringt Code Mapping Klarheit in Softwareprojekte, besonders wenn man neu im Team ist. Führt man das Mapping alle ein bis zwei Monate erneut durch, erkennt man früh, ob man noch auf Kurs ist oder gegensteuern sollte.
Eine Karte macht es auch einfacher, nicht nur Ziele, sondern auch Kursänderungen nachvollziehbar zu kommunizieren. Deshalb lohnt es sich, Code Mapping einfach einmal auszuprobieren.
Anhang – Arbeiten mit CodeCharta
Der Einstieg in CodeCharta ist dank des neuen unifiedparser unglaublich einfach.
Er nutzt die leistungsstarke Tree-Sitter-Bibliothek , um Quellcode direkt, also ohne vorheriges Kompilieren, zu analysieren.
# Überprüfen, ob java installiert ist
java --version ## "java 21.0.2" oder ähnlich
# Überprüfen, ob npm installiert ist
npm --version ## "10.9.2" oder ähnlich
# CodeCharta-Analysetools herunterladen
npm install -g codecharta-analysis
# Projekt deiner Wahl klonen
git clone git@github.com:laravel/framework.git laravel-framework
# Projekt analysieren
ccsh unifiedparser laravel-framework -o laravel-framework.uni
# CodeCharta Web Studio öffnen und Datei laden
open https://codecharta.com
CodeCharta bietet außerdem mehrere weitere Parser für unterschiedliche Quellen: einen Parser für Git (ccsh gitlogparser), einen Importer für SonarQube (ccsh sonarimport) und natürlich ein Tool, um die Ergebnisse zusammenzuführen (ccsh merge). Wenn dich das Projekt interessiert, lohnt sich ein Blick auf die Projektseite – und wenn du richtig Lust hast, kannst du auch mithelfen. Es gibt noch einiges, das entwickelt werden kann.
Anhang – Alternativen zu CodeCharta
CodeCharta ist natürlich nicht das einzige Tool, mit dem sich Code visualisieren und analysieren lässt.
CodeScene nutzt beispielsweise kreisförmige Packungen für seine Darstellungen und bewertet automatisch die Code Health einer Codebasis. Seerene bietet ebenfalls eine Karte, kombiniert diese jedoch mit einem digitalen Boardroom, der CTOs einen Überblick über das gesamte Unternehmen ermöglicht.
SonarGraph eignet sich hervorragend, um Code zu kartieren und problematische Abhängigkeiten zu identifizieren. Seine Visualisierungen sind unter anderem im Buch Langlebige Software-Architekturen zu sehen. Eine Alternative war Structure101, das inzwischen in Sonar integriert wurde.
JetBrains Quodana und SonarQube wiederum sind statische Codeanalyse-Tools, die Probleme in Dashboards darstellen – nicht als Karten. Mit dem passenden Importer lassen sich ihre Ergebnisse aber ebenfalls in CodeCharta visualisieren.