How to Tech-Stack: Technologieauswahl für langlebige Projekte
Aktuelle Software-Systeme sind häufig komplexe Systeme, die im Team erstellt werden. Für deren Umsetzung stehen Technologien zur Verfügung, die den Entwicklern wiederkehrende Aufgaben erleichtern. Darunter fallen Datenbankzugriffe, Netzwerkkommunikation zwischen Teilsystemen, Generierung oder Umwandlung von Dateiformaten, finanzielle Berechnungen und vieles mehr. Die Entscheidung einzusetzender Technologien gestaltet sich dabei schwierig. Häufig kommen für die Umsetzung von technischen Anforderungen verschiedene Optionen in Frage, wobei es die "beste" Wahl nicht gibt. Sie passen in ihre Lösungen besser oder schlechter zum aktuellen Projekt. Es liegt in der Verantwortung aller fachlich und technisch Mitwirkenden, diese Technologien in einem gut passenden Tech-Stack zusammenzustellen.
Die Erstellung und Wartung eines Tech-Stacks kann mühsam sein. Die jeweils "perfekte" Entscheidung zu treffen ist arbeits- und zeitintensiv und aus Gründen von Budget- und Zeitgrenzen in der Praxis nicht umsetzbar. Glücklicherweise ist dies nicht nötig, denn es gibt kritische und unkritische Teile des Tech-Stacks, was einen unterschiedlichen Fokus auf diese Bereiche erlaubt. Dadurch wird Zeit und Energie frei, die in kritische Entscheidungen für den Aufbau des Tech-Stacks fließen kann.
Dieser Artikel präsentiert einige Faustregeln bzw. Heuristiken, die zu guten Lösungen führen, aber nicht immer ein Optimum darstellen. Die Heuristiken hängen von den gewünschten Systemeigenschaften ab. Für hochperformante Systeme wird es andere Heuristiken geben als für langlebige Systeme. Bei den langlebigen Systemen, auf die sich dieser Artikel bezieht, können sich viele Eigenschaften der Umgebung grundlegend ändern: Anforderungen, Frameworks, Technologien und Paradigmen müssen in der Nutzungszeit geändert oder vollständig ersetzt werden. So kann es durch Änderungen in den Umgebungen zu Inkompatibilitäten kommen oder es sind Sicherheitslücken zu schließen. Wenn am Anfang des Projekts nicht feststeht, für welche Nutzungsdauer das System erstellt wird, ist es eine gute Standardannahme, von langlebigen Systemen auszugehen. Der Erfahrung nach entwickeln sich viele Systeme zu einem langlebigen System, auch wenn das nicht von Anfang an geplant war.
Ein Beispiel für ein langlebiges System mit seinen Herausforderungen ist eine Mitte der 2010er gestartete Software-Entwicklung, welche auf viele damals aufstrebende Technologien aufbaut. Nachdem die Anwendung in einer ersten Version fertiggestellt war, wurden einige Jahre lang im Wesentlichen Bugs bzw. Sicherheitslücken behoben. Nach dieser Zeit der minimalen Wartung mussten größere Änderungen durchgeführt werden, da sich die Anforderungen an die Anwendung geändert haben. Plötzlich war die Not groß: Betreiber der eingesetzten Technologien stellten die Weiterentwicklung oder gar die Wartung komplett ein. Hystrix [1] ist eine eingesetzte Technologie, die offiziell eingestellt wurde. Viel Aufwand war notwendig, um die eingesetzte Software-Erosion zu bändigen.
Der Tech-Stack ist der Oberbegriff aller in der und für die Software eingesetzten Technologien. Der Begriff Technologie ist dabei weit gefasst und umfasst die Bereiche Plattformen, Bibliotheken, Laufzeitumgebungen und Buildtools. Plattformen sind Software-Bestandteile, die einer Software oder einem Programm grundlegende Strukturen geben und grundlegende Funktionen bereitstellen. Dazu gehören in der Java-Welt u. a. Spring (Boot), Quarkus oder in der JavaScript-Welt Node.js. Im Gegensatz dazu liefern Bibliotheken Funktionalitäten, die aus anderen Modulen eingebunden werden, wie OR-Mapper, Logging-Frameworks oder Circuit-Breaker. Zu den Laufzeitumgebungen gehören alle Systembestandteile, die zur Ausführung benötigt werden. Das können Betriebssysteme, Message-Broker oder Datenbanken sein. Auch Runtimes wie die .NET-Laufzeitumgebung oder die Java-Runtime-Umgebung gehören in diesen Bereich. Die letzte Gruppe umfasst alle Tools, die im Endprodukt nicht mehr erkennbar sind, aber in die Erstellung einer Software involviert sind. Dazu gehören insbesondere CI/CD-Tools (z. B. Jenkins) und Compiler (z. B. javac), aber auch qualitätssichernde Werkzeuge wie SonarQube und Tools zur Performance-Analyse bzw. zur Durchführung von Lasttests.
Die nächsten Abschnitte beschreiben die Heuristiken für die langlebige Nutzung erwähnter Technologien, die in vier verschiedene Bereiche eingegliedert sind.
Passende Technologie
Technologien sind in heutigen Systemen die grundlegenden Bausteine einer Software. Die Qualität der Auswahl hat direkte Auswirkungen auf die Wartbarkeit und die Qualität der Software.
Heuristik: Nutze Technologien mit einer aktiven Community
Langlebige Projekte benötigen reife Technologien, um eine solide Basis zu schaffen. Hierfür sind vornehmlich zwei Aspekte wichtig:
Reife Technologien sind in der Regel intuitiv nutzbar, gut erlernbar und besitzen eine gute Dokumentation. Sowohl der Erfahrungsschatz der Technologie-Hersteller als auch das Feedback von Anwendern führen zu kontinuierlichen Verbesserungen. Nicht umsonst ist es häufig so, dass auch die Dokumentationen als Open Source bereitgestellt werden (z. B. die Dokumentation von Github [2]), um Rückmeldungen aus der Nutzerschaft zu fördern.
Für unerfahrene Benutzer ist es wichtig, schnell Hilfe zu bekommen. Beispielsweise über den Austausch von Erfahrungen und Problemstellungen bei Plattformen wie Stackoverflow [3]. Insbesondere für eine schnelle und qualitative Beantwortung von Fragen ist es vorteilhaft, wenn es viele Anwender gibt, die einander helfen. Dies erhöht weiterhin die Wahrscheinlichkeit, dass sich die Entwickler im Projektteam mit dieser Technologie bereits auskennen und sich gegenseitig unterstützen bzw. dass diese Kenntnisse im Bedarfsfall in das Unternehmen eingebracht werden können (im Rahmen von Schulungen oder mittels externer Coaches).
Ein zweiter Aspekt ist – speziell bei Open-Source-Projekten – die Motivation der Betreiber. Neben wirtschaftlichen Aspekten besteht ihre Motivation darin, einen Nutzen für die Gemeinschaft zu schaffen oder sich selbst einen Namen zu machen. Haben die Betreiber das Gefühl, dass dies nicht (mehr) der Fall ist, besteht eine erhöhte Gefahr, dass sie das Projekt nicht weiterführen bzw. weiterführen können. Im Zweifel finden sich keine Personen, die das Projekt übernehmen, wie dies bspw. 2022 im JavaScript-Bereich mit den Projekten Reach UI [4] und react-keycloak [5] geschehen ist .
Ein Hinweis über die Nutzung einer Technologie lässt sich über die Anzahl von GitHub-Sternen oder die Anzahl der Downloads bzw. Forks näherungsweise ableiten. Aktivitäten in den Issue-Diskussionen können einen guten Einblick sowohl in die Motivation der Maintainer als auch in die Zufriedenheit der Nutzer geben.
Heuristik: Nutze saubere Technologien
Es ist selten, dass eine Technologie gut gebaut ist und gleichzeitig Tests, Dokumentation etc. schlecht sind. Dies liegt daran, dass Entwickler, die sauber und umsichtig arbeiten, dies nicht auf den Code beschränken. Daher ist es eine gute Heuristik (und leider keine Regel), sich alle Bereiche einer Technologie anzusehen. Wenn die Webseite, ReadMe-Dateien, Hilfen etc. gut gepflegt sind, deutet das auf eine gepflegte Technologie hin. Solche sauberen Technologien haben gut strukturierte und intuitiv verständliche Schnittstellen, die das Arbeiten vereinfachen und so Fehler in der Nutzung vermeiden. Ein Qualitätskriterium ist eine Dokumentation, die die Migration auf eine neuere Version der Technologie beschreibt. Besonders angenehm ist eine Unterstützung bei der Versionsmigration durch die IDE oder Tools wie OpenRewrite [6].
Heuristik: Achte auf die genutzten Lizenzen
Lizenzmodelle beschreiben, wie eine Technologie genutzt werden darf. Bei GPL-Lizenzen ist die Nutzung auf verschiedene Arten eingeschränkt. Die Verletzung von Lizenzrechten kann dazu führen, dass die damit verbundene Nutzung umgehend eingestellt werden muss und ggf. Schadensersatzansprüche geltend gemacht werden. So fordert die Verbindung von GPL-Technologien aufgrund ihres strengen Copylefts, dass die verwendete Software ebenfalls unter GPL gestellt werden muss. Eine Möglichkeit, genutzte Lizenzen im Überblick zu behalten, sind Tools wie "License Maven Plugin", ein Maven-Plugin [7], das Lizenzen automatisiert auflistet, soweit die Informationen sauber und maschinell verarbeitbar in den Technologien gepflegt sind.
Andererseits kann die Lizenz eingesetzter Technologien die Übernahme des Codes oder abgrenzbarer Teile dessen erlauben, sodass diese zukünftig selbst gepflegt werden können. So erlaubt beispielsweise die Apache-2-Lizenz die Übernahme in kommerziellen Code. Auf diese Weise konnte aus dem abgekündigten Hystrix die Funktionalität des Message Collapsing in die eigene Anwendung übernommen werden, die über die ersetzende Bibliothek Resilience4J[8] nicht abgedeckt ist. Im Rahmen des Projekts wurde dieser Teilbereich auf die eigenen Bedürfnisse optimiert. Daher ist es günstig, frühzeitig auf die Lizenz zu achten und diese im Projektverlauf regelmäßig im Blick zu behalten.
Heuristik: Analysiere Alternativen und verwende nicht das Erste, das jemand kennt
Vorschnelle bzw. implizite Entscheidungen zur Nutzung einer Technologie können zu einem ungünstigen Tech-Stack führen. Dies schließt den Einsatz von mehreren Technologien mit ein, welche die gleiche Funktion liefern. Das Einbinden ist durch moderne Build- und Dependency-Tools (bspw. Gradle, MSBuild/NuGet, NPM…) einfach und schnell. Selbst bei Entscheidungen im Team wird bisweilen vorschnell ein Vorschlag aufgenommen, der in die Runde geworfen wird. Dabei zeigt sich in manchen Teams die Tendenz, entweder zu alte oder zu aktuelle Technologien zu nutzen (s. auch die Heuristiken "Werde untote Technologien los" und "Nutze keine Bleeding-Edge-Technologie").
In Maven-Central stehen insgesamt weit über eine halbe Million Pakete zur Verfügung. Daher sind Entscheidungen für langlebige Projekte bewusst zu treffen. Genauere Vorschläge hierfür finden sich im Abschnitt "Klare Entscheidungen”.
Heuristik: Nutze keine "Bleeding-Edge-Technologie"
Frisch aufkommende, gut beworbene und für die Entwicklergemeinde spannend aussehende Technologien sind nicht immer eine gute Wahl für langlebige Projekte. Natürlich macht es uns Entwicklern am meisten Spaß, mit möglichst neuen Technologien zu arbeiten. Jedoch haben diese (noch) einen schlechten Support. Die Ersteller fokussieren sich darauf, die Entwicklung der Technologie und die frühen Rückmeldungen aus der Nutzerschaft einzubinden. Dementsprechend fehlen Zeit und Ressourcen für die zugehörige Dokumentation. Die Technologie befindet sich im starken Umbruch, sodass sie häufig grundlegend umgestellt wird. Solche "Breaking Changes" finden den Weg schleppend in die Dokumentation und bedeuten für das einsetzende Projekt, dass auf die geänderten APIs umgestellt werden muss. Gleichzeitig fehlt es im Team an Erfahrung mit der neuen Technologie. Die Kombination dieser Punkte erhöht die Wahrscheinlichkeit, dass sich der Einsatz nach anfänglichem Spaß und Erfolgserlebnissen als mühsam erweist. Erschwerend kommt hinzu, dass derartige Technologien häufig in Konkurrenz zu ebenfalls aufkommenden Mitbewerbern entstehen. Welche dieser Technologien sich durchsetzen und weiterentwickeln werden, ist schwer abzusehen.
Wenn auf das falsche Pferd gesetzt wurde, also eine Technologie beispielsweise nicht mehr weiter gepflegt wird und daher entfernt werden soll (s. Heuristik "Werde untote Technologien los"), werden aufwändige Umbauten nötig.
Heuristik: Vermeide die Einbindung unnötiger Technologien
Der Einsatz von Technologien birgt auch einige Nachteile, die sich bei nicht benötigten Technologien negativ auswirken können. Hierfür sollte man sich vor Augen führen, welchen Anteil Technologien in der fertiggestellten Software ausmachen: Analysen des Contrast Labs aus 1.857 Software-Systemen ergaben, dass die Programme im Schnitt nur ca. aus einem Fünftel selbst geschriebenem Code bestehen [9]. Der andere Teil besteht aus eingebundenen Bibliotheken, wobei der genutzte Anteil weniger als ein Zehntel der Anwendung ausmacht. Das bedeutet: Im Schnitt verbleiben mehr als zwei Drittel des vollständigen Programmcodes ungenutzt. Da Bibliotheken nicht den Spezialfall der eigenen Anwendung abdecken, sondern möglichst allgemeingültige Funktionen anbieten, ist dies nachvollziehbar.
Komplexe oder sicherheitskritische Umsetzungen (bspw. Kryptographie) sind für eine Eigenimplementierung zu herausfordernd. Jedoch gibt es Funktionalitäten, die gut maßgeschneidert selbst umgesetzt werden können – oder es findet sich eine Bibliothek, die möglichst passgenau die benötigte Funktionalität abdeckt. Weiterhin überschneiden sich verschiedene Bibliotheken in der Funktionalität (bspw. Erstellung von REST-Schnittstellen). Hier sollte der Einsatz auf eine Bibliothek beschränkt werden, um eine konsistente Nutzung und eine gegenseitige Beeinflussung der Bibliotheken zu vermeiden.
Ein Beispiel dafür, dass eine komplexe Technologie durch eine geschickte Anwendung einer anderen genutzten Technologie ersetzt wurde, ist die Ersetzung von Zuul, wenn nur einfache Weiterleitungen genutzt werden. Die genutzte Funktionalität konnte durch Templates für den schon eingesetzten OpenApi-Generator ersetzt werden.
Transitivität vermeiden
Transitive Abhängigkeiten sind nicht direkt eingebundene Bibliotheken, sondern werden durch andere Bibliotheken oder Frameworks eingebunden. Diese Indirektion erzeugt in verschiedenen Szenarien Herausforderungen, von denen drei genauer betrachtet werden (Abb. 1).
- Sicherheitslücken und Bugs (Abb. 1 A) können jede Bibliothek betreffen. Wenn so ein Problem in einer transitiven Bibliothek auftritt, ist die Behebung nicht direkt möglich. Am einfachsten ist es, wenn die einbindende Bibliothek oder Framework eine neue Version bereitstellt, die eine gefixte Bibliothek mitbringt. Wenn dies nicht der Fall ist, ist das Ziel, die transitive Bibliothek auszutauschen, was voraussetzt, dass die nutzende Bibliothek mit der neuen Version kompatibel ist.
- Probleme mit der Kompatibilität (Abb. 1 B) können auch in anderen Szenarien auftreten. Wenn eine Bibliothek durch verschiedene Bibliotheken in verschiedenen Versionen transitiv eingebunden wird, kann es durch inkompatible Versionen zu Fehlern kommen. Wenn Technologien weiter gepflegt werden, kommen neue Funktionen hinzu und alte werden abgekündigt (deprecated) bzw. entfernt. Daher können verschiedene Versionen einer Technologie unterschiedliche APIs haben oder sich im Verhalten unterscheiden. Wenn zwei Abhängigkeiten verschiedene Versionen einer transitiven Abhängigkeit nutzen, kann es zu Compile- oder Funktionsfehlern kommen.
- Die dritte hier betrachtete Herausforderung tritt auf, wenn zwei transitive Bibliotheken nicht miteinander harmonieren (Abb. 1 C). Sie kann beispielsweise hervorgerufen werden, weil sie untereinander inkompatibel sind oder wenn sie eine stark überschneidende Arbeitsweise besitzen. Beispielsweise gab es Schwierigkeiten bei früheren Versionen von Mapstruct und Lombok, die beide Code durch Annotationen ersetzen und teilweise den Code der anderen Technologie nicht beachtet haben [10].
Insgesamt zeigt sich, dass transitive Bibliotheken Herausforderungen mit sich bringen. Zwar versuchen verschiedene Bündelungen (BOMs, Starter-POMs, etc.) die Herausforderungen zu minimieren, dennoch ist es ratsam, unnötige Transitivitäten zu vermeiden. Aus diesen Beobachtungen leiten sich Heuristiken für transitive Abhängigkeiten ab:
Heuristik: Nutze Bibliotheken mit wenigen transitiven Abhängigkeiten
Je weniger transitive Abhängigkeiten es gibt, desto unwahrscheinlicher ist das Auftreten der genannten Herausforderungen. Daher sollten Bibliotheken, die weniger Abhängigkeiten haben, bevorzugt werden. Manche Bibliotheken sehen einen Plugin-Mechanismus vor. So bietet Hibernate die Möglichkeit, Funktionen durch Einbinden von optionalen Bibliotheken wie Caching über Ehcache zu erweitern [11].
Heuristik: Bibliotheken direkt einbinden (nicht transitiv)
Ziel ist es, direkt die genutzten Funktionalitäten einzubinden und nicht die Funktionalität gemeinsam mit vielen zusätzlichen nicht genutzten Fähigkeiten zu vermischen. Häufig entstehen solche Fälle, wenn ein Entwickler in einem Projekt eine Funktionalität sucht und schnell die eingebundene Bibliothek aus einem vorigen Projekt übernimmt. Dabei ist es nicht selten, dass die gewünschte Funktionalität nicht in der Bibliothek selbst enthalten ist, sondern in einer der transitiven Abhängigkeiten. Die direkte Nutzung kann die übergeordnete Bibliothek funktional vollständig ersetzen (s. Abb. 2).
Klare Entscheidungen
Oft wundert man sich nach Jahren, welche Gründe es für konkrete Entscheidungen gab. Diese zu hinterfragen, gestaltet sich häufig schwierig. Einzelne Personen wissen eventuell Teilaspekte, aber niemand erinnert sich an die Hintergründe der Entscheidung. In diesem Kontext fallen häufig Aussagen wie "historisch gewachsen".
Wie risikoreich ist es, in diesen Situationen eine getroffene Entscheidung umzuwerfen? Gut, wenn zur damaligen Zeit vorgedacht und die damaligen Kriterien und Begründungen niedergeschrieben wurden.
Empfehlung: Architecture Decision Records zur Dependency-Verwaltung einsetzen
Eine Möglichkeit dies umzusetzen sind sogenannte ADRs (Architecture Decision Records) [12]. Im Wesentlichen wird über diese dokumentiert, welche Problemstellungen oder Anforderungen (funktional oder nicht funktional) bestehen, welche Qualitätsszenarien damit verbunden sind und welche Entscheidungsmöglichkeiten in Betracht gezogen wurden. Darin ist die endgültig getroffene Entscheidung und die zugehörige Begründung dokumentiert. Hierüber entwickelt sich über die Zeit ein ADR-Log, über den jeder im Projekt jederzeit nachvollziehen kann, welche Gründe für die Entscheidungen vorlagen und diese ggf. zum derzeitigen Software-Stand neu bewerten kann.
Empfehlung: Nutze Entscheidungsmatrizen
Entscheidungsmatrizen stellen verschiedene Optionen für eine Entscheidung mithilfe der Entscheidungskriterien gegenüber [13]. In der Matrix oder Tabelle werden die Bewertungen eingetragen. Aufgrund der übersichtlichen Darstellung sind sie ein geeignetes Mittel, um direkt in ADRs eingebunden zu werden.
Tabelle: Vereinfachte Entscheidungsmatrix am Beispiel zweier Mapping-Frameworks
Kriterium | Mapstruct | Dozer | |||
Bezeichnung | Gewichtung | Punkte | gew. Punkte | Punkte | gew. Punkte |
Mapping via Annotation | 3 | 5 | 15 | 5 | 15 |
No Additional Runtime dependency | 4 | 5 | 20 | 1 | 4 |
35 | 19 |
Entscheidungsmatrizen (s. Tabelle) sammeln alle Optionen (im dargestellten Beispiel in Spalten) und definieren zum Projekt passende Kriterien (im dargestellten Beispiel in Zeilen). Durch eine Gewichtung wird die Wichtigkeit des Kriteriums für das Projekt definiert. Beim formalen Vorgehen wird mit Zahlen/Punkten eine Auswertung durchgeführt. In der im Beispiel verwendeten Gegenüberstellung zweier im Java-Bereich infrage kommender Bibliotheken zur Transformierung zwischen Entitäten und Transferobjekten ist der Einsatz von Mapstruct im Beispielprojekt aufgrund der niedrigeren Abhängigkeiten zur Laufzeit gegenüber Dozer als vorteilhafter anzusehen.
In der Praxis ist das formale Vorgehen meist unnötig und kann bei ungeübten Durchführenden zu Fehlschlüssen führen. Als informelle Dokumentationsmethode spielen Entscheidungsmatrizen auch leichtgewichtig ihre Vorteile aus. Dabei werden die Optionen und Kriterien wie gehabt dokumentiert. Nun werden die wichtigsten Bewertungspunkte in die Felder geschrieben und je nach Wertung mit den drei Farben grün (vorteilig), gelb (neutral) und rot (nachteilig) eingefärbt. Dieser letzte Schritt kann gut in Diskussionen zur Dokumentation und Bewertung der Argumente genutzt werden. Wenn ein neues Kriterium oder eine neue Option in der Diskussion aufkommt, wird dieses einfach ergänzt.
Heuristik: Generatoren statt Reflection und Tool-Magie
Bei vielen Ansätzen wie Mappern oder Schnittstellen gibt es zwei Schulen, wie diese durch Bibliotheken angebunden werden können. Die eine versucht dynamisch durch Reflection, klassisches AOP (Aspektorientierte Programmierung) [14] oder Annotation Processing die Funktionalität einzubinden. Die andere Schule generiert den benötigten Code, der dem normalen Kompilationsprozess unterzogen wird.
Wer schon einmal Code mit Reflection oder AOP versucht hat zu debuggen weiß, wie schwierig das Nachvollziehen der genauen Funktionsweise ist. Fehlern auf den Grund zu gehen, ist meist erst zur Laufzeit im Debugger möglich. Generierter Code ist konkreter und ohne Debugger mit den Techniken der IDE analysierbar. Bei einigen Tools, wie beispielsweise beim OpenAPI-Generator [15], kann die Codeerzeugung durch Templates gesteuert werden, was es erlaubt, zusätzlich eigenen Code zu generieren. Dies ermöglicht weitergehende Anpassungen an die eigenen Bedürfnisse und kann die Nutzung anderer Technologien überflüssig machen. Die Themen wie eine frühzeitigen Erzeugung von Code oder eine frühzeitige Auflösung von Dependency Injection sind inzwischen so präsent, dass in Frameworks wie Quarkus [16] vieles schon zur Compilezeit aufgelöst wird.
Da diese Heuristik teilweise nicht die einzige Entscheidungsgrundlage ist, sollte sie bei einer Entscheidungsmatrix als Kriterium einfließen.
Aktuell und sauber bleiben
Für die Erstellung und Instandhaltung der eigenen Software gibt es einige Kriterien. Eine genutzte Technologie braucht während des Einsatzes Pflege. Sie wird weiterentwickelt und Fehler werden ausgebessert. Daher sollte beim verwendeten Tech-Stack regelmäßig kontrolliert werden, ob die Auswahl noch stimmig und aktuell ist.
Heuristik: Werde untote Technologien los
Technologien sind untot, wenn diese zwar noch verfügbar sind, aber nicht mehr gewartet werden. Sie haben die Herausforderung, dass Anpassungen an neue Technologien sowie Bug- und Security-Fixes nicht mehr durchgeführt werden. Anwendungen mögen mit veralteter Technologie noch eine ganze Zeit lang funktionieren, allerdings bauen sich von selbst technische Schulden auf: Wenn es später zu einem Problem kommt und die Technologie ausgetauscht werden muss, wird es dafür umso unangenehmer. Da sich Technologien zusammen entwickeln, müssen häufig auch andere Teile der Software angepasst werden.
Wenn es zu einem Problem kommt und die Technologie ausgetauscht werden muss, wird es umso unangenehmer.
Untote Technologien können verhindern, dass andere Technologien aktualisiert werden können, weil sie beispielsweise die gleichen transitiven Abhängigkeiten verwenden. Weil die neue Version der noch lebenden Technologie eine Version der transitiven Abhängigkeit voraussetzt, die mit der Version der untoten Technologie nicht kompatibel ist, altern diese beiden gemeinsam und verschlimmern das Problem zusätzlich.
Heuristik: Halte deine Technologien aktuell
Kleine Schritte sind einfacher als große Revolutionen. Moderne Technologien ändern sich graduell. Große Versionen mit vielen Änderungen sind seltener geworden. Viele Technologien haben kurze Release-Zyklen und Patches von Sicherheitslücken werden auf die aktuellen Versionen beschränkt. Wenn Software-Systeme regelmäßig die Technologien aktualisieren, sind wenige inkompatible Änderungen schnell angepasst. Zu langes Warten resultiert in vielen durchzuführenden Änderungen und wegen der Abhängigkeiten von anderen Technologien in einer großen Update-Orgie. Besonders unangenehm ist dies, wenn eine schwerwiegende Sicherheitslücke durch ein Update geschlossen werden muss und dadurch solche Aktionen unter Zeitdruck geraten.
Beim Aktualisieren von Technologien ist es hilfreich, wenn sich die Entwickler an Semantic Versioning halten [17]. Hier kann die Änderung der Versionsnummer Aufschluss geben, wie groß die Gefahren von Breaking Changes sind.
Besondere Herausforderungen stellen Technologien, die bei großen Änderungen nicht die Version ändern, sondern auf ein neues Artefakt gehen. Diese Aktualisierungen können nicht durch automatisierte Tools oder die IDE festgestellt werden, sondern müssen meist manuell erfolgen. Geschehen ist dies bei RxJava, dessen Weiterentwicklung augenscheinlich mit der Version 1.3.8 endete, aber unter neuen GruppenIds (rxjava2 bzw. rxjava3) fortgesetzt wurde.
Heuristik: Sei pingelig und achte auf Kleinigkeiten
Je weniger Ausnahmen und je weniger Abweichungen von Standard-Arbeitsweisen in den Technologien bestehen, desto einfacher sind Upgrades oder Änderungen an den Technologien durchzuführen. In einigen Fällen lässt sich ein Upgrade der Technologien automatisieren.
Wenn Entwickler ihre Module sauber pflegen und sauber alle Build-Informationen ablegen, machen sie es ihren Kollegen (für eine automatisierte Nutzung) einfacher. Diese Aspekte gehören zu den angesprochenen Kleinigkeiten. In diesem Sinne ist diese Heuristik das Gegenstück zu "Nutze saubere Technologien" aus dem Bereich "Passende Technologie". Arbeite so sauber, wie du es von anderen erwartest.
Heuristik: Kapsele deine Technologie – wenn möglich – durch Module
Technologien, die nicht in der ganzen Anwendung benötigt werden, sollten durch eine gute Architektur in ihrem Einflussbereich gekapselt werden. Ziel ist dabei, die Auswirkungen einer Änderung in der eigenen Anwendung auf einen möglichst kleinen Bereich zu beschränken. Dies ist nicht möglich, wenn es sich um grundlegende Plattformen und Frameworks wie Spring handelt, da diese grundlegende Funktionen in der Software bereitstellen und daher in nahezu allen Modulen benötigt werden.
Bei Technologien wie z. B. Hibernate oder Circuit Breakern ist dies jedoch gut möglich. Zu den Strategien gehören die Kapselung in Modulstrukturen oder die Erzeugung des genutzten Codes durch Generatoren.
In einigen Architekturen ist eine Kapselung von technischen Bibliotheken inhärent vorgesehen. So ist in der hexagonalen Architektur durch die konsequente Nutzung von Adaptern eine natürliche Strukturierung gegeben (Abb. 4). Dabei genügt es, darauf zu achten, dass die Nutzung der Bibliotheken wirklich durch die Adapter gekapselt ist und sich nicht durch den Code zieht.
Wenn die Funktionalität einer Bibliothek in verschiedenen Modulen benötigt wird, kann diese Nutzung durch ein separates Modul gekapselt werden. Abb. 5 zeigt, wie die Nutzung von Resilience4J bei verschiedenen Adaptern gekapselt wird. Das Modul übernimmt die Bereitstellung der Funktionalität, ohne die Bibliothek direkt an die anderen Module zu binden.
Die dritte Möglichkeit für die Kapselung ist die Injektion von Funktionen mittels Codegeneratoren oder AOP in die anderen Module, um eine explizite Nutzung zu vermeiden.
Für alle obigen Optionen bleibt die Herausforderung sicherzustellen, dass die Kapselung in den Projekten eingehalten wird. Hierzu bietet sich ArchUnit [18] an, das festgelegte Regeln durch Unit-Tests sicherstellt.
Fazit
Ein Tech-Stack soll die Entwicklung der damit erstellten Anwendung optimal unterstützen. Dabei können passendere Alternativen bestehen. Die eingesetzten Technologien können durch Lizenzprobleme, Sicherheitslücken und Inkompatibilitäten verschiedene Projektrisiken mit sich bringen. Heuristiken vereinfachen den Aufbau und die Wartung eines Tech-Stacks und eignen sich zur Bewertung und Verbesserung bestehender Tech-Stacks. Sie stellen Leitlinien dar, die dem Team oder Architekten helfen, fokussiert über die für den Projekterfolg kritischen Teile des Tech-Stacks nachzudenken. Damit vereinfachen sie Entscheidungen und führen gleichzeitig zu brauchbaren Ergebnissen. Heuristiken sind Faustregeln, die oft, aber nicht immer passen. Erfahrungen für den Aufbau eines Tech-Stacks und in Verbindung mit den Projektanforderungen werden damit nicht ersetzt und sind weiterhin wichtig. Ausschließlich Anforderungen, die komplexe Lösungen erfordern, benötigen tiefgehende Recherchen und Entscheidungsmethoden. Die Erfahrung und Mitarbeit im Team wird auch zukünftig ein wesentlicher Bestandteil für gute Software-Architekturen bleiben. Die Nutzung der Heuristiken schafft die Zeit und den Freiraum, an den kritischen Technologien zu arbeiten.
- Github: Hystrix
- Github
- Stackoverflow
- Github: Reach UI
- Github: React Keycloak
- OpenRewrite
- MojoHaus: License Maven Plugin
- Github: Resilience4J
- Contrast Lab: Contrast Study finds that less than 10% of Application Code is Active third-party Library Code
- Github: Mapstruct + Lombok failed to generate implementation of mappers
- Ehcache
- Heise online: Gut dokumentiert: Architecture Decision Records
- BWL-Lexikon: Entscheidungsmatrix
- Wikipedia: Aspektorientierte Programmierung
- OpenAPI Generator
- Quarkus
- SemVer: Semantic Versioning
- ArchUnit: Unit test your Java architecture