Dreidimensionales Browserfenster mit verbundenen Code-, Daten- und Funktionsbausteinen in Dunkelblau, Weiß und Rot
7 Min. Lesezeit

Modulare Webarchitektur: damit eine Änderung nicht alles anfasst

Modulare Webarchitektur am Beispiel einer Veranstaltungsanmeldung: klare Zuständigkeiten, verlässliche Buchungen und Änderungen ohne Kettenreaktion.

Ein Formular, ein freier Platz, eine Bestätigungsmail: Die Anmeldung zu einer Veranstaltung klingt nach einer überschaubaren Funktion. Dann soll eine Warteliste dazukommen. Plötzlich müssen das Formular, drei Datenbankabfragen, die E-Mail-Vorlage und die Verwaltungsansicht gleichzeitig geändert werden. Niemand ist sicher, wo eigentlich entschieden wird, ob eine Anmeldung gültig ist.

Modulare Webarchitektur soll solche Änderungen eingrenzen. Ein Modul bündelt eine fachliche Verantwortung und bietet anderen Teilen der Anwendung einen klaren Zugang dazu. Die Zahl der Dateien ist dabei zweitrangig. Entscheidend ist, ob eine Regel einen erkennbaren Ort hat und ob andere Teile sie benutzen können, ohne ihre Umsetzung kennen zu müssen.

An einer beispielhaften Veranstaltungsanmeldung lässt sich zeigen, wie diese Grenzen entstehen – und wo selbst ein ordentliches Architekturdiagramm noch keine verlässliche Anwendung ergibt.

Mit einer Änderung beginnen

Eine hilfreiche Einstiegsfrage lautet: Welche Anforderung könnte nächste Woche dazukommen? Bei der Anmeldung wären das eine Warteliste, eine Stornofrist oder ein anderer Anbieter für Bestätigungsmails. Diese Änderungen betreffen unterschiedliche Entscheidungen.

Die Warteliste verändert die Buchungsregeln. Ein anderer Mailanbieter verändert die Zustellung. Eine neue Darstellung freier Plätze verändert zunächst die Oberfläche. Wenn alle drei Wünsche dieselbe große Funktion berühren, sind dort vermutlich mehrere Verantwortungen zusammengewachsen.

Für das Beispiel reicht eine kleine Aufteilung:

Bereich Verantwortet Bietet anderen Bereichen an
Veranstaltungen Termin, Titel und veröffentlichte Informationen Veranstaltungsdaten lesen
Buchungen Kapazität, Anmeldungen, Warteliste und Stornierung Anmelden, stornieren, Verfügbarkeit abfragen
Benachrichtigungen Vorlagen, Versandversuche und Zustellstatus Eine Bestätigung zur Zustellung vormerken
Oberfläche Eingaben, Darstellung und Rückmeldungen Bedienbare Abläufe für Gäste und Verwaltung

Die Kapazität liegt hier bewusst bei den Buchungen. Das Veranstaltungsmodul darf einen Veranstaltungstitel ändern, aber keine zusätzlichen Plätze vergeben. Auch die Verwaltung passt die Kapazität über das Buchungsmodul an, damit dessen Regeln weiterhin gelten.

Das ist eine mögliche Aufteilung für dieses Beispiel. Ein reiner Veranstaltungskalender ohne Anmeldung bräuchte diese Grenzen nicht in derselben Form.

Ein Modul braucht einen erkennbaren Eingang

Die Oberfläche sollte nicht selbst mehrere Tabellen verändern, um eine Anmeldung zusammenzubauen. Sie übergibt einen Auftrag an einen serverseitigen Anwendungsfall, etwa anmelden(veranstaltungId, teilnehmerId, anfrageId).

Dieser Einstieg prüft Berechtigung und Eingaben, wendet die Buchungsregeln an und veranlasst die Speicherung. Zurück kommt ein fachliches Ergebnis: bestätigt, auf Warteliste oder abgelehnt. Die Oberfläche übersetzt es in eine verständliche Rückmeldung.

Wie sich solche Eingaben und Ausgaben in TypeScript gezielt ableiten lassen, zeigt der Leitfaden zu Utility Types mit Pick, Omit und Record. Die Typen dokumentieren den Vertrag; die Prüfung eingehender Daten bleibt eine Laufzeitaufgabe.

Damit werden zwei Entscheidungen getrennt: Was ist passiert? beantwortet das Buchungsmodul. Wie zeigen wir es? beantwortet die Oberfläche. Eine Datenbankfehlermeldung ist kein geeigneter Vertrag zwischen beiden.

Innerhalb des Buchungsmoduls können Regeln, Ablaufsteuerung und Datenbankzugriff getrennt sein. Dafür braucht es anfangs keine große Sammlung abstrakter Klassen. Häufig reichen wenige Funktionen und ein eindeutig benannter öffentlicher Einstieg. Weitere Bereiche greifen über diesen Einstieg zu; die internen Hilfsfunktionen bleiben intern.

Eine Ordnerstruktur hilft, diese Absicht sichtbar zu machen. Verlässlich wird sie erst, wenn das Team die Abhängigkeiten auch prüft: Darf das Veranstaltungsmodul interne Buchungstabellen beschreiben? Darf die Oberfläche eine Regel umgehen? Solche Zugriffe sollten im Review auffallen oder durch Importregeln im Build verhindert werden.

Der letzte freie Platz gehört in die Datenbankentscheidung

Jetzt wird es interessant: Zwei Personen sehen gleichzeitig „Noch ein Platz frei“ und klicken auf Anmelden. Beide Anfragen lesen einen freien Platz, beide speichern eine Buchung. Die Oberfläche war ordentlich getrennt. Trotzdem ist die Veranstaltung überbucht.

Kapazität prüfen und einen Platz vergeben müssen zusammen gegen konkurrierende Buchungen abgesichert sein. Eine Transaktion allein genügt je nach Datenbank und Isolationsniveau nicht. Der Speicherzugriff muss beispielsweise passende Sperren oder eine atomare, bedingte Änderung nutzen. Die konkrete Umsetzung hängt vom Datenmodell ab. Die PostgreSQL-Dokumentation zur Transaktionsisolation erläutert, welche Garantien die einzelnen Isolationsstufen bieten.

Die Zuständigkeit bleibt klar: Das Buchungsmodul verlangt, dass seine Kapazitätsregel unter Parallelzugriff gilt. Sein Datenbankzugriff setzt diese Garantie um. Ein deaktivierter Button im Browser ist dabei eine Bedienhilfe, keine Absicherung.

Ein zweiter Fall entsteht bei einer schlechten Verbindung. Die Buchung wurde gespeichert, die Antwort erreicht den Browser aber nicht. Beim Wiederholen darf nicht versehentlich eine weitere Buchung entstehen. Dafür lässt sich eine Anfrage-ID verwenden, deren Verarbeitung und Ergebnis dauerhaft gespeichert werden. Dieselbe ID mit demselben Auftrag liefert dann das bereits bekannte Ergebnis. Wird sie mit anderen Daten wiederverwendet, muss der Server den Konflikt erkennen. Die ID muss außerdem an die berechtigte Person gebunden sein.

Dieses Verhalten heißt Idempotenz: Die Wiederholung desselben Auftrags erzeugt keinen zusätzlichen fachlichen Effekt. Sie ersetzt nicht die eigene Regel, ob eine Person überhaupt mehrere Plätze buchen darf.

Eine fehlende E-Mail darf keine Buchung verschwinden lassen

Eine Anmeldung wurde bestätigt, aber der Mailanbieter ist nicht erreichbar. Welchen Status sieht der Gast jetzt?

Für unser Beispiel bleibt die Buchung gültig. Die Oberfläche zeigt die Bestätigung samt Buchungsreferenz. Der Versand bekommt einen eigenen Status und kann wiederholt werden. „Buchung bestätigt“ und „Bestätigungsmail versandt“ sind zwei verschiedene Tatsachen.

Die naive Reihenfolge „Buchung speichern, danach E-Mail versenden“ hat allerdings eine Lücke: Stürzt die Anwendung zwischen beiden Schritten ab, bleibt eine Buchung ohne Versandauftrag zurück. Eine mögliche Lösung ist eine Outbox: Buchung und Versandauftrag werden in derselben Datenbanktransaktion gespeichert. Ein Hintergrundprozess arbeitet die offenen Aufträge ab. Das Transactional-Outbox-Muster in der AWS-Dokumentation beschreibt dieses Vorgehen und seine Grenzen.

Auch damit ist die externe Zustellung nicht automatisch exakt einmal garantiert. Ein Prozess kann nach erfolgreichem Versand abstürzen, bevor er den Auftrag als erledigt markiert. Beim Wiederholen droht dann eine zweite Mail. Eine stabile Versand-ID und die Duplikatbehandlung des Anbieters können helfen; ohne entsprechende Unterstützung muss die Anwendung mit diesem Fall umgehen können.

Der wichtige Entwurfsschritt ist, die Fehler getrennt sichtbar zu machen. Die Buchungsübersicht braucht den Buchungsstatus. Der Betrieb braucht zusätzlich fehlgeschlagene Versandversuche und eine Möglichkeit, sie gezielt erneut auszuführen. Im Artikel über Architektur beim Fehlerfall geht es um dieselbe Frage bei Medienverarbeitung und Suche.

Gemeinsam deployen, gezielt trennen

Die beschriebenen Module können zunächst in einer Anwendung und mit einer gemeinsamen Datenbank laufen. Das ist ein modularer Monolith: eine gemeinsam ausgelieferte Anwendung mit bewusst begrenzten internen Zuständigkeiten.

Martin Fowler beschreibt in „Monolith First“, warum frühe Servicegrenzen schwierig sind: Zu Beginn ist oft noch unklar, welche Funktionen fachlich zusammengehören. Innerhalb einer Anwendung lassen sich solche Grenzen leichter verschieben als zwischen unabhängig betriebenen Diensten. Er stellt das als Erfahrungsargument mit Grenzen dar, nicht als allgemeingültiges Gesetz.

Für unsere Anmeldung wäre ein eigener Netzwerkdienst für jede Tabellenzeile schwer zu begründen. Ein separat laufender Versandprozess kann dagegen sinnvoll sein, wenn Zustellversuche unabhängig abgearbeitet werden sollen. Dafür muss nicht gleich das gesamte Benachrichtigungsmodul als eigener Service entwickelt werden.

Eine Auslagerung wird interessant, sobald ein konkreter Bedarf entsteht: eigene Skalierung, andere Laufzeitbedingungen oder ein Team, das einen Bereich tatsächlich unabhängig veröffentlicht. Die zusätzlichen Aufgaben – Netzwerkfehler, Authentifizierung zwischen Diensten, Überwachung und Versionsverträglichkeit – gehören dann zur Entscheidung. Googles SRE-Kapitel „Simplicity“ begründet, warum unnötige Komplexität auch ein Betriebsproblem ist.

Tests an den Übergängen ansetzen

Die wertvollsten Prüfungen folgen den Versprechen der Module. Für die Anmeldung ergeben sich daraus fünf konkrete Fälle:

  1. Kapazität: Zwei gleichzeitige Anfragen auf den letzten Platz führen höchstens zu einer bestätigten Buchung. Diese Prüfung läuft gegen die tatsächlich verwendete Datenbank.
  2. Wiederholung: Derselbe Auftrag mit derselben Anfrage-ID erzeugt keine zweite Buchung und keinen zweiten Versandauftrag.
  3. Berechtigung: Eine Person kann keine fremde Buchung stornieren, auch wenn sie deren ID kennt.
  4. Versandfehler: Ein Ausfall des Mailanbieters lässt die bestätigte Buchung bestehen und den Versandauftrag erneut bearbeitbar.
  5. Gesamtablauf: Ein Gast kann sich anmelden, das Ergebnis verstehen und die eigene Buchung später wiederfinden.

Eine reine Regelprüfung ist schnell und braucht keinen Browser. Der Parallelzugriff braucht einen Integrationstest mit der Datenbank. Für den letzten Fall lohnt sich ein durchgängiger Oberflächentest. So prüft jede Ebene etwas, das die anderen nicht zuverlässig abdecken.

Eine bestehende Anwendung schrittweise ordnen

Bei einer gewachsenen Anwendung würde ich mit genau einem Ablauf beginnen: etwa dem Absenden der Anmeldung. Zuerst wird das heutige Verhalten durch passende Tests festgehalten. Dann bekommt der Ablauf einen gemeinsamen Einstieg. Die bisherigen Aufrufer werden nacheinander darauf umgestellt, anschließend lassen sich verstreute Kopien derselben Regel entfernen.

Der nächste kleine Auftrag liefert die Probe: Kommt eine Warteliste hinzu, bleibt die Kapazitätsentscheidung im Buchungsmodul. Die Oberfläche bekommt einen zusätzlichen Ergebniszustand. Der Versand erhält eine passende Nachricht. Mehrere Dateien dürfen sich dabei ändern – entscheidend ist, dass jeder Bereich einen nachvollziehbaren Teil der Änderung übernimmt.

Das Titelbild zeigt verbundene Bausteine. In einer wartbaren Anwendung bekommen diese Verbindungen eine konkrete Bedeutung: einen Auftrag, ein Ergebnis und ein Verhalten für den Fehlerfall. Genau dort lohnt es sich, beim nächsten Umbau anzufangen.