Strukturen für das Frontend: von Featuremodulen bis zu hexagonaler Architektur

Bereits früh im Verlauf der Anwendungsentwicklung eine Architektur zu implementieren, die dann mitwächst, bewahrt ebenso vor Overengineering wie vor einem am Ende unübersichtlichen Moloch. Wir schauen uns die Möglichkeiten an und bringen dabei das Tool Sheriff zum Einsatz.

Rainer Hahnekamp
Rainer Hahnekamp

Rainer Hahnekamp ist Trainer und Berater im Expertennetzwerk von AngularArchitects.io und dort für Schulungen rund um Angular verantwortlich. Darüber hinaus gibt er mit ng-news auf YouTube einen wöchentlichen Kurzüberblick über relevante Ereignisse im Angular-Umfeld.

Mit Softwarearchitekturen verhält es sich ähnlich wie mit dem Testen. Jeder weiß, dass man nicht ohne auskommt. Zu Beginn, wenn man noch in einer prototypenhaften Phase steckt, schenkt man der Architektur meist noch nicht so viel Beachtung. Jedes Element kann auf alles zugreifen und man kommt so schnell zu ersten Resultaten. Die Anwendung wächst und wächst, bis sie irgendwann so groß ist, dass man den Überblick verliert. Dadurch, dass jeder mit jedem „reden“ darf, verursacht man mit jeder Änderung auch gleich einen neuen Bug, bis die Entwicklung ins Stocken gerät.

Zu diesem Zeitpunkt erinnert man sich an die schön strukturierten Architekturpläne, die diese Probleme verhindert hätten. Nur ist es jetzt zu spät. Eine Umstrukturierung würde so viele Änderungen und somit Bugs nach sich ziehen, dass man das Risiko nicht eingehen möchte. Ein Teufelskreis.

Im Folgenden möchte ich gängige Architekturen vorstellen, wie wir sie in einer klassischen Frontend-Anwendung bspw. bei Angular vorfinden. Dabei sortieren wir die Modelle nach Anwendungsgröße. Wir starten mit einer kleinen Anwendung und schauen uns an, welche Anforderungen sich an die Architektur mit steigender Größe ergeben und wie man darauf reagiert. Das hat den Vorteil, dass man mit einem einfachen Modell starten kann und bereits bestens für die nächste Stufe vorbereitet ist. Dabei bleiben wir ausschließlich bei einem monolithischen System. Das Thema Microfrontends würde den Rahmen dieses Artikel sprengen.

Minimale Architektur

Nehmen wir an, wir haben eine sehr einfache Anwendung. Eine Website für eine Reiseagentur zum Beispiel. Am Anfang umfasst sie neben der Startseite ein Impressum und eine Seite mit Kontaktdaten. Das ist nicht viel. Impressum und Kontakt sind jeweils zwei Komponenten mit statischem Text und es werden keine Services benötigt. Auf der Startseite sollen aktuelle News angezeigt werden.

Für die Kommunikation mit einem Backend bzw. einer statischen .json-Datei mit den Newsdaten reicht ein einfacher Service. Weiterhin wird der Router verwendet. Falls wir uns den Luxus von Mehrsprachigkeit leisten wollen, kommt noch eine der gängigen i18n-Bibliotheken wie transloco oder das offizielle Modul des Angular-Frameworks hinzu. Bei derartigen Anwendungen brauchen wir uns über Architektur noch keine Gedanken zu machen. Es kommt alles direkt in das Projektverzeichnis.

Features und Shared-Module

Nehmen wir nun jedoch an, dass zwei weitere Funktionen hinzukommen. Wir möchten eine Übersicht über Städtetrips und eine kleine Listenansicht bestehender Kunden ergänzen. Das sind bereits Funktionen, die über die Kernbestandteile hinausgehen. Als Kernbestandteile kann man die Shell, die Startseite sowie die Footer-Seiten bezeichnen.

Mit den beiden neuen Funktionen ist der Zeitpunkt erreicht, ab dem eine Architektur mit eigenen Regeln empfehlenswert ist. Warum? Nun, wir möchten zwar kein Overengineering betreiben, aber zumindest die Grundstrukturen für die Module vorbereiten. Aktuell setzen wir ein Feature mit einem Modul gleich. Diese Vorgabe erlaubt uns, später neue Features (bspw. einen Onlineshop) hinzuzufügen, und gleichzeitig haben wir einen separaten Ordner, in dem wir unsere bestehenden Features ausbauen können, ohne den Rest der Anwendung damit zu belasten. Wenn wir also einen Ordner für die sogenannten Holidays haben, können wir dort weitere Services, Komponenten usw. hinzufügen und behalten trotzdem den Überblick.

Es werden sich schnell Services oder Komponenten ergeben, die von beiden Features verwendet werden sollen. Damit diese erkennbar als featurelos oder generisch gekennzeichnet sind, geben wir sie in einen separaten Ordner, den wir Shared nennen. Auch diesen Ordner sehen wir als Modul an.

Unsere App besteht nun aus drei Modulen. Das sind die Holidays, die Customers und Shared. Jedem Modul ist dabei ein eigener Ordner zugeteilt. Das ist unser Fundament, auf dem wir mit fortschreitender Anwendungsgröße aufbauen können (Abb. 1).

Abb. 1: Applikation mit Featuremodulen

Encapsulation

Diese Unterteilung der Ordnerstruktur nach Features und einem Shared ist eine der gängigsten. Wir sind jedoch noch nicht fertig. Im Weiteren wollen wir mit einem wesentlichen Aspekt eines Moduls starten, nämlich mit der Encapsulation.

Wir haben nun Module. Sie sind aber nicht nur dafür da, eine übersichtliche Ordnerstruktur widerzuspiegeln. Es soll auch möglich sein, dass ein Modul sich einzelne Services, Komponenten etc. nur für interne Zwecke bewahrt und den Zugriff von außen verweigert. Aktuell ist das nicht so. Man kann jederzeit bspw. von Customers auf Holidays zugreifen. Theoretisch könnte somit Customers einen Service von Holidays verwenden und selbstständig neue Holidays-Elemente anlegen etc. Wie stellen wir sicher, dass das nicht passiert? Man kann das bei einer Codereview zwar immer manuell überprüfen, doch ist hierbei das Risiko sehr hoch, dass man es in stressigen Zeiten das eine oder andere Mal übersieht.

Automatische Qualitätssicherung

Es muss also eine Automatisierung her. Derartige Tools gibt es für verschiedene Programmiersprachen und Plattformen. Ein sehr bekanntes Beispiel aus dem Java-Bereich ist ArchUnit, das die Qualitätssicherung in Rahmen von Unit-Tests durchführt.

Im JavaScript-Umfeld hat sich für derartige Zwecke das sogenannte Linting durchgesetzt. Dies erlaubt es, ad hoc Qualitätschecks auszuführen, wodurch man bereits während des Schreibens des Quelltexts von der IDE eine entsprechende Fehlermeldung bekommen kann. Im Umfeld von Angular würde sich hier Nx anbieten. Daneben gibt es noch generischere Lösungen wie dependency-cruiser.

In diesem Artikel soll allerdings ein wesentlich schlankeres Tool verwendet werden, dass unter dem Namen Sheriff bekannt ist und für jedwede TypeScript-Anwendung eingesetzt werden kann.

Konfiguration von Sheriff

Die Voraussetzung ist, dass ESLint mit dem Sheriff-Plug-in installiert und konfiguriert ist. Bei einem Angular-CLI-basierten Projekt müsste man folgende Kommandos ausführen:

npx ng add @angular-eslint/schematics

npm install –save-dev @softarc/eslint-plugin-sheriff

Danach noch die Regel in der Datei .eslintrc.json aktivieren und Sheriff ist einsatzbereit.

Die Encapsulation basiert bei Sheriff auf Konventionen. So muss jedes Modul in irgendeiner Art und Weise definieren, welche Elemente von außen erreichbar sind. In TypeScript oder JavaScript ist das normalerweise eine index.ts- bzw. index.js-Datei. Sie markieren das API für den entsprechenden Ordner samt Unterordner. Diese Konvention hat aber normalerweise technisch keinerlei Bedeutung. Man kann nach wie vor bei einem Import die index.ts-Datei ignorieren und direkt auf die entsprechende Datei innerhalb eines Moduls referenzieren.

Sheriff behandelt jedoch jedes Verzeichnis, das eine index.ts-Datei aufweist als ein Modul. Das heißt, alle Importe von außerhalb müssen auf sie referenzieren. Alles andere würde einen Linting-Fehler produzieren. Damit hat man ohne irgendwelche Konfigurationseinstellungen und durch den Einsatz bestehender Konventionen bereits eine automatische Überprüfung der Encapsulation eingebaut (Abb. 2).

Für das Beispiel werden also jeweils drei index.ts-Dateien für die Module Customers, Holidays sowie Shared erstellt. Bei Featuremodulen ist es normalerweise so, dass sie in Angular lazy über das Routingsystem geladen werden.

Aus diesem Grund wird in den beiden index.ts-Dateien lediglich das Routes-Objekt exportiert. Beim Shared-Modul ist es anders. Da es eher dafür gedacht ist, von Featuremodulen verwendet zu werden, wird der Großteil, wenn nicht sogar alles, exportiert.

Abb. 2: Linting-Fehler wegen Verletzung der Encapsulation

Abb. 2: Linting-Fehler wegen Verletzung der Encapsulation

Multiple Features und Domains

Nach einiger Zeit wird man mehr und mehr Features hinzuprogrammiert haben. Dabei stellt sich größtenteils heraus, dass sich sogenannte Featuregruppen bilden. Das heißt, dass wir neben Holidays und Customers vielleicht noch andere Features haben. Innerhalb von Holidays gibt es ein Feature zur Buchung, ein weiteres, um Kommentare zu hinterlassen, oder eines, um sich Bilder von vergangenen Reisen anzusehen.

Wir werden die Featuregruppen im weiteren Verlauf Domains nennen. Denn das ist es, was sie wirklich sind. Mittlerweile hat sich der Ansatz des Domain-driven Design durchgesetzt, in dem man seine Anwendung nach Domains unterteilt und diese aus einzelnen Features bestehen. Ganz vereinfacht ausgedrückt, bildet die Domain ein Gruppe von Benutzern ab, die in demselben Bereich arbeiten, was zum Beispiel innerhalb einer Organisation eine Fachabteilung sein könnte. Diese Gruppen verwenden untereinander eigene Begriffe, deren Bedeutung innerhalb der Gruppe dieselbe ist. Dabei kann es auch sein, dass eine andere Fachabteilung dieselben Begriffe verwendet, jedoch im Kontext einer anderen Aufgabe. Somit wäre die andere Fachabteilung eine eigene Domain und der Begriff würde pro Domain eigenständig verwendet werden. Für mehr Informationen über Domain-driven Design empfiehlt der Autor die Lektüre der Fachliteratur.

Domain-Module

Wir sind nun bereits bei einer dreifachen Hierarchie angelangt. Wo am Anfang die Anwendung und ihre Featuremodule waren, finden wir nun die Anwendung, die Domains und erst darunter die Features. Im Weiteren müssen wir uns über Domain-einheitlichen Code Gedanken machen. Das wären auf jeden Fall einmal die Domain-Modelle selbst, wie zum Beispiel der Typ einer Holidays- oder Customer-Domain mit Properties und Validierungsregeln.

Zum anderen kann man davon ausgehen bzw. ist zu hoffen, dass sich die Domains bereits im API wiederfinden. Dadurch hat man domänenspezifische Services, die den Zugriff auf das API abwickeln. Mit einer wachsenden Anzahl von Features kann dieser Service auch durch ein komplettes State Management à la NgRx abgelöst werden.

Wie dem auch sei, wir haben nun innerhalb einer Domain mehrere Featuremodule und ein Kernmodul, woraus sich die Features bedienen. Daneben dürfen wir nicht vergessen, dass wir noch unser Shared-Modul haben. Auch hier wird es notwendig sein, es in weitere kleine Module aufzuteilen. So kann man zum Beispiel ein Shared-Modul haben, das Spezialkomponenten für Fomularfelder anbietet, ein anderes kümmert sich um das Error Handling, das Logging oder ist für die Authentifizierung zuständig. Wir kommen somit auf eine neue Evolutionsstufe, die skizzenhaft aussehen kann wie in Abbildung 3. Im Endeffekt ist das die Referenzarchitektur in Angular, wie sie von Manfred Steyer bekannt gemacht wurde.

Abb. 3: Domains mit Featuremodulen

Abhängigkeitsregeln und Tags

Mit wachsender Anwendungsgröße steigen auch die Anforderungen an unser Automatisierungstool für Architekturchecks. Encapsulation ist bei dieser Anzahl von Modulen nicht mehr ausreichend. War es bei ausschließlichen Featuremodule noch möglich, dass alle gleichgestellt waren, gibt es nun eine Hierarchie.

Eine Hierarchie erfordert wiederum Abhängigkeitsregeln. Wie in einem Unternehmen auch, möchte man vielleicht nicht, dass ein Mitarbeiter einen Vorgesetzten einer anderen Abteilung direkt kontaktiert und dadurch Kommunikationsprozesse am direkten Vorgesetzten vorbei stattfinden. In einem Unternehmen wird man vielleicht noch ein bisschen toleranter sein. Bei einer technischen Hierarchie wie in unserer Anwendung müssen diese Regeln jedoch streng eingehalten werden. Was kann man hier also tun?

Wie bereits vorher erwähnt, gibt es im Angular-Umfeld den von Nx bekannt gemachten Ansatz der Tags und der darauf aufbauenden Abhängigkeitsregeln. Nachdem wir bereits Sheriff einsetzen, ist es natürlich wünschenswert, dass wir hier nicht das Tool wechseln müssen. Das ist auch nicht nötig. Im Gegensatz zur Encapsulation müssen wir hier jedoch selbst Hand anlegen. Wir müssen alle unsere Module mit generischen Tags versehen und aufgrund dieser Tags dann Abhängigkeitsregeln aufbauen. So kann man zum Beispiel Module innerhalb einer Domain als solche markieren und dann eine Regel einführen, nach der Module nicht Domain-übergreifend kommunizieren dürfen.

Sheriff bietet hierbei eine Konfigurationsdatei an, in der man sowohl die Vergabe der Tags als auch der Regeln festlegen kann. Der Idealfall wäre, dass man nun alle Domain-Module nach denselben Ordnerstrukturen aufbaut. Dann benötigt man nur eine Konfigurationseinstellung und muss nicht für jede neue Domain wieder eingreifen.

Nehmen wir an, sämtliche Domains sind als Unterordner von Domains angelegt und man hätte jeweils zwei Features pro Domain sowie einen Domain-Kern, der als data bezeichnet wird. Man käme auf die Ordnerstruktur, die in Abbildung 4 zu sehen ist. Die zugehörige Sheriff-Konfiguration würde aussehen, wie in Listing 1 gezeigt.

Abb. 4: Ordnerstruktur Domain- und Featuremodule

Listing 1: sheriff.config.ts

import { SheriffConfig } from '@softarc/sheriff-core';
 
export const config: SheriffConfig = {
  version: 1,
  tagging: {
    'src/app/': {
      children: {
        shared: { tags: 'shared' },
        'domains/{domain}': {
          tags: (_, { placeholders: { domain } }) => [`domain:${domain}`],
          children: {
            data: { tags: 'type:data' },
            'feat-{type}': { tags: 'type:feature' },
          },
        },
      },
    },
  }
};

Der Code sieht auf den ersten Blick ein wenig kompliziert aus, ist es aber nicht. Mit der tagging Property können wir dynamisch und mittels Platzhalter Tags zuweisen. Mit der hierarchischen Struktur definieren wir mittel des Keys ’src/app‘, dass wir die Tags auf alle Unterordner in diesem Verzeichnis anwenden wollen.

Wir starten mit dem Verzeichnis shared, das eine Sonderrolle einnimmt und den gleichnamigen Tag bekommt. Mit domains/{domain} definieren wir einen Platzhalter für das unmittelbare Unterverzeichnis. Dieser Schlüssel hat zwei Properties. Mit tags wird eine Funktion aufgerufen, die über die Variable placeholder den Verzeichnisnamen erhält. Diesen ziehen wir gleich heran, um allen Modulen den Tag domain:[Verzeichnisname] zuzuordnen.

Dasselbe Spiel geht dann in den nächsten Unterordnern weiter. Nachdem das Verzeichnis data immer das Kernmodul ist, markieren wir es mit type:data. Bei den Verzeichnissen, die mit feat- beginnen, verwenden wir den Tag type:feature. Wir können nun so viele Domains anlegen, wie wir möchten. Solange wir uns an diese Verzeichniskonvention halten, müssen wir die Tagkonfiguration nicht mehr ändern.

Was wir zudem sehen, ist, dass hier offenbar zwei Arten von Tags verwendet werden. Es gibt diejenigen, die mit domain: und jene, die mit type: beginnen. Das hat natürlich einen Grund. Jedes Modul wird anhand von zwei Dimension kategorisiert und darauf bauen wir die Abhängigkeitsregeln auf. Man kann natürlich mehr Dimensionen verwenden. Allerdings muss man dann beachten, dass man für jede Dimension auch neue Abhängigkeitsregeln definieren muss.

Bleiben wir bei zwei Dimensionen. Wir möchten, dass bei der Dimension domain nur Module mit demselben Wert aufeinander zugreifen können. Dazu kommt die Spezial-Domain domain:shared, auf die wiederum alle Domains Zugriff haben. Zusätzlich gelten dann weitere Regeln für die einzelnen Typen. Wo die domain-Dimension die Abhängigkeiten zwischen den Domains regelt, geht type eine Ebene tiefer und fokussiert sich auf die Abhängigkeiten innerhalb der Domain. So wollen wir, dass die Featuremodule zwar auf das Kernmodul (data) zugreifen können, aber nicht umgekehrt. Zudem sollen die Featuremodule sich auch nicht gegenseitig aufrufen können. Ehrlicherweise müsste man shared als dritte Dimension bezeichnen. Das ist jedoch, wie bereits angeführt, ein Sonderfall. In Sheriff würden die Regeln für beide Dimensionen wie in Listing 2 definiert


Software Architecture Camp – Foundation Flexible

In diesem intensiven Training erwirbst du die Fähigkeiten, um klare Anforderungen in robuste Softwarestrukturen umzusetzen.

iSAQB®-Zertifizierung:

Lassen Sie sich direkt nach dem Workshop unabhängig zum CPSA-F zertifizieren.

Nächster Termin

17. Nov bis 19. Dez 2025, online

Foundation Camp + Workshop Soft Skills

In diesem intensiven Training erwirbst du die Fähigkeiten, um klare Anforderungen in robuste Softwarestrukturen umzusetzen.

iSAQB®-Zertifizierung:

Lassen Sie sich direkt nach dem Workshop unabhängig zum CPSA-F zertifizieren.

Nächster Termin

10. bis 14. Nov 2025, Berlin

Modul ADOC – Architekturdokumentation

Vermittelt die Grundlagen, um Architekturen klar zu dokumentieren und effektiv zu kommunizieren.

Kompetenz:

  • Methodisch: 20 Credits

Nächster Termin

23. bis 24. Apr 2026, München

Modul AGILA – Agile Softwarearchitektur

Zeigt, wie agile Prinzipien mit der Arbeit als Softwarearchitekt:in kombiniert werden können.

Kompetenz:

  • Methodisch: 20 Credits
  • Kommunikativ: 10 Credits

Nächster Termin

08. bis 10. Dez 2025, München

Modul ARCEVAL - Architekturen bewerten & reflektieren

Vermittelt die systematische Bewertung von Softwarearchitekturen und deren Qualitätsmerkmalen.

Kompetenz:

  • Methodisch: 20 Credits

Nächster Termin

21. bis 22. Apr 2026, München

Modul CLOUDINFRA – Infra., Container + Cloud Native

Du lernst, wie du moderne Cloud-Infrastrukturen skalierbar entwirfst, implementierst und betreibst.

Kompetenz:

  • Technisch: 20 Credits
  • Methodisch: 10 Credits

Nächster Termin

03. bis 05. Nov 2025, München

Modul DDD – Domain-driven Design

Zeigt, wie Softwaremodelle effektiv entwickelt werden können, um komplexe Domänen abzubilden.

Kompetenz:

  • Methodisch: 20 Credits
  • Kommunikativ: 10 Credits

Nächster Termin

08. bis 10. Dez 2025, online

Modul DSL – Domänenspezifische Sprachen

Lerne, wie du sie konzipierst und entwickelst – von der Analyse der Fachdomäne bis zur Umsetzung in klaren, wartbaren DSLs.

Kompetenz:

  • Technisch: 10 Credits
  • Methodisch: 20 Credits

Modul FLEX – Flexible Architekturmodelle​

Behandelt Methoden und Strategien für skalierbare, modulare und anpassungsfähige Softwarearchitekturen.

Kompetenz:

  • Technisch:   20 Credits
  • Methodisch: 10 Credits

Nächster Termin

01. bis 04. Dez 2025, online

Modul FUNAR – Funktionale Softwarearchitektur

Erklärt Prinzipien wie unveränderliche Daten, algebraische Abstraktionen und funktionale Modellierung.

Kompetenz:

  • Technisch:   20 Credits
  • Methodisch: 10 Credits

Modul IMPROVE – Evolution und Verbesserung von Architekturen

Zeigt, wie bestehende Softwarearchitekturen analysiert und gezielt optimiert werden können.

Kompetenz:

  • Technisch:  10 Credits
  • Methodisch: 20 Credits

Nächster Termin

17. bis 19. Nov 2025, Berlin

Modul SOFT – Soft Skills für Softwarearchitekt:innen

Vermittelt essenzielle Kompetenzen wie Kommunikation, Moderation und Konfliktmanagement.

Kompetenz:

  • Kommunikativ: 30 Credits

Nächster Termin

05. bis 07. Nov 2025, München

Modul SWARC4AI – Softwarearchitektur KI-Systeme

Behandelt Grundlagen und Methoden zur Gestaltung moderner Softwarearchitekturen für KI-Systeme.

Kompetenz:

  • Technisch: 20 Credits
  • Methodisch: 10 Credits

Nächster Termin (Durchführungsgarantie)

11. bis 13. Nov 2025, München

Modul WEBSEC – Web-Security

Zeigt, wie Sicherheitsanforderungen in Webanwendungen systematisch adressiert werden können

Kompetenz:

  • Methodisch: 20 Credits
  • Kommunikativ: 10 Credits

Nächster Termin

10. bis 12. Nov 2025, Berlin

Alle News zum Software Architecture Camp!