flowchart LR
kunde(["Kunde: Person"])
zahlung["Zahlungsdienst: externes System"]
subgraph sys["Buchungssystem"]
web["Web-App: React"]
buchung["Buchung-Service: Python"]
flotte["Flotte-Service: Python"]
dbb[("Buchung-DB: Postgres")]
dbf[("Flotte-DB: Postgres")]
end
kunde -->|"nutzt per HTTPS"| web
web -->|"ruft auf"| buchung
buchung -->|"fragt Verfügbarkeit"| flotte
flotte -->|"fragt gebuchte Zeiträume"| buchung
buchung -->|"liest und schreibt"| dbb
flotte -->|"liest und schreibt"| dbf
buchung -->|"löst Zahlung aus"| zahlung
Architekturstile, Trade-offs und Architecture Decision Records
Track Konzepte · Design und Architektur · ca. 65 Min.
Worum es geht
“Sollen wir das als Microservices bauen?” ist eine Frage, die du als Freelancer ständig hörst. Die ehrliche Antwort ist fast nie “ja” oder “nein”, sondern “kommt darauf an, und zwar auf diese vier Dinge”. Genau diese Dinge lernst du hier: Architektur ist keine Liste schöner Muster, sondern eine Folge von Entscheidungen mit Kosten (Trade-offs). Jeder Stil gibt dir etwas und nimmt dir etwas.
Am Ende der Lektion kannst du die fünf gängigen Architekturstile (architecture styles) einordnen: Layered, Hexagonal/Clean, Event-driven, modularer Monolith und Microservices. Du formulierst Qualitätsattribute (quality attributes) als messbare Szenarien statt als Schlagwort (“schnell”), du schreibst einen Architecture Decision Record (ADR), und du schützt eine Architekturregel mit einem automatischen Test, einer Fitness Function.
Das ist eine bekannte Lücke von dir (Monolith gegen Microservices). Darum gehen wir den Weg erst einmal komplett durch gerechnete Beispiele, bevor du selbst entscheidest. Nichts davon braucht ein Netzwerk oder Docker: Code-Beispiele laufen im Browser, Diagramme sind Text (Mermaid).
Plane ehrlich 65 Minuten ein: etwa 25 Minuten Lesen, dann fünf Übungen (zwei davon ohne automatischen Check, mit Musterlösung zum Selbstvergleich).
Von JS/TS her gedacht
Du hast Architektur schon gebaut, nur selten so genannt:
| Stil | Wie es in deinem JS/TS-Alltag aussieht |
|---|---|
| Layered | Ordner routes/, services/, repositories/: jede Schicht ruft nur die darunter auf |
| Hexagonal / Clean | Ein interface im Kern (z. B. RabattQuelle), die echte Datenbank oder fetch-Variante steckt außen und wird hereingereicht |
| Event-driven | EventEmitter, Redux-Actions, oder Nachrichten über eine Queue (siehe Lektion 06) |
| Modularer Monolith | Ein Repository mit Workspace-Paketen (packages/billing, packages/catalog), Importregeln per Lint (z. B. mit einem ESLint-Plugin für Modulgrenzen, bitte prüfen) |
| Microservices | Mehrere deploybare Projekte mit eigener API, eigener Pipeline und eigenem Datenspeicher |
Dependency Injection (Abhängigkeit hereinreichen) kennst du aus React (Props, Context) und aus Tests (jest.fn() als Fake). Die Idee hinter Hexagonal ist genau das, nur auf Architekturebene. Dasselbe in TypeScript und in Python, ausgeführt mit Node und Python:
function versandkosten(gewichtKg: number, tarifQuelle: (land: string) => number): number {
return Math.round(gewichtKg * tarifQuelle("DE") * 100) / 100;
}
console.log(versandkosten(4, (land) => 2.5));
console.log(versandkosten(0.5, () => 3.0));Ausgabe mit Node (als JavaScript ohne Typen ausgeführt):
10
1.5
Python gibt 10.0 aus, JavaScript 10 (JS kennt nur eine Zahlenart). Wichtig ist etwas anderes: versandkosten kennt weder Datenbank noch Netzwerk. Wer sie aufruft, entscheidet, woher der Tarif kommt. Das merken wir uns für Schritt 5.
Konzept
Schritt 1: Qualitätsattribute als Szenarien, nicht als Schlagworte
“Das System soll skalierbar und performant sein” ist keine Anforderung, sondern ein Wunsch. Niemand kann prüfen, ob er erfüllt ist. Ein Qualitätsattributs-Szenario (quality attribute scenario) hat vier Teile:
| Teil | Frage | Beispiel |
|---|---|---|
| Quelle (source) | Wer oder was löst es aus? | 500 Kunden gleichzeitig |
| Stimulus (stimulus) | Was passiert? | rufen im Schlussverkauf die Produktsuche auf |
| Reaktion (response) | Was tut das System? | liefert Ergebnisse |
| Messgröße (measure) | Woran siehst du, ob es reicht? | 95 % der Anfragen in unter 300 ms |
Merke: Ohne Messgröße mit Zahl und Einheit ist es noch ein Schlagwort. Die Messgröße ist auch das, was du später mit einem Lasttest oder einer Metrik (Lektion 09) prüfen kannst. Szenarien sind der Eingang zur Trade-off-Analyse: Die meisten Szenarien ziehen in verschiedene Richtungen. Wer 99,99 % Verfügbarkeit will, zahlt mit Betriebsaufwand. Wer Änderungen in einer Stunde in Produktion will, zahlt mit Testautomatisierung. Die Methode ATAM (Architecture Tradeoff Analysis Method, steht in der Quelle) geht genau so vor: Szenarien sammeln, gewichten, Architekturoptionen dagegen halten.
Schritt 2: Die fünf Stile im Überblick
| Stil | Kernidee | Gibt dir | Kostet dich |
|---|---|---|---|
| Layered | Schichten, Aufrufe nur nach unten | Einfach, jeder kennt es | Änderungen ziehen oft durch alle Schichten, Geschäftslogik rutscht in die Datenbankschicht |
| Hexagonal / Clean | Der Kern (Geschäftslogik) kennt nur Ports (Schnittstellen), Adapter außen setzen sie um | Kern ohne Datenbank testbar, Technik austauschbar | Mehr Schnittstellen und Übersetzungscode |
| Event-driven | Komponenten reagieren auf Ereignisse statt sich aufzurufen | Lose Kopplung, neue Konsumenten ohne Änderung am Sender | Ablauf schwer nachzuvollziehen, Idempotenz und Reihenfolge (Lektion 06) |
| Modularer Monolith | Ein Deployment, innen fachlich getrennte Module mit erzwungenen Grenzen | Einfacher Betrieb, echte Transaktionen, Grenzen später noch verschiebbar | Disziplin nötig, sonst wird es ein Knäuel; skaliert nur als Ganzes |
| Microservices | Viele kleine Dienste, je eigenes Deployment und eigene Daten | Teams deployen und skalieren unabhängig, Ausfälle bleiben begrenzt | Netzwerk, verteilte Daten, Betrieb, Tests über Servicegrenzen |
Die Stile schließen sich nicht aus. Hexagonal beschreibt, wie der Code innen geschnitten ist, Microservices, wie er ausgeliefert wird. Ein modularer Monolith kann innen hexagonal sein und über Events mit einem Worker reden.
Schritt 3: Monolith gegen Microservices, durchgerechnet
Ein Microservice ersetzt einen Funktionsaufruf durch einen Netzwerkaufruf. Der kann langsam sein, fehlschlagen, doppelt ankommen (Lektionen 06 und 07). Rechne ein, was das für die Verfügbarkeit heißt. Angenommen, jeder Service ist für sich zu 99,9 % verfügbar, und eine Anfrage braucht n Services nacheinander und alle (keine Fallbacks). Dann multiplizieren sich die Wahrscheinlichkeiten (unter der Annahme, dass Ausfälle unabhängig sind):
Zehn Services in Reihe haben statt rund 8,8 Stunden Ausfall im Jahr rund 87. Das ist der Preis der Verteilung, wenn du ihn nicht mit Timeouts, Retries und Fallbacks abfängst. Es ist ein Modell mit Annahmen, keine Messung. Aber es zeigt die Richtung.
Damit wird die Antwort auf den Prüfstein greifbar. Ein Monolith (genauer: ein modularer Monolith) ist die bessere Wahl, wenn:
- Das Team klein ist (ein Team, eine Handvoll Leute). Unabhängige Deployments lösen ein Koordinationsproblem, das du dann nicht hast.
- Die Grenzen noch unklar sind. Eine falsche Grenze im Monolith ist ein Refactoring. Eine falsche Grenze zwischen Services ist ein Projekt mit Datenmigration und Versionierung.
- Du starke Konsistenz brauchst, etwa Bestellung und Zahlung in einer Transaktion (Lektion 01). Über Services brauchst du Sagas (Lektion 05).
- Niemand Betrieb für viele Dienste leisten kann (Pipelines, Monitoring, Tracing pro Service).
Microservices lohnen sich, wenn mehrere Dinge zusammenkommen: stabile fachliche Grenzen, mehrere Teams, die sich beim Release gegenseitig bremsen (Conway’s Law: die Struktur des Systems spiegelt die Kommunikationsstruktur der Organisation), unterschiedliche Skalierung einzelner Teile und ein Team oder eine Plattform, die den Betrieb trägt. Fehlen diese Voraussetzungen, bekommst du die Kosten ohne den Nutzen (“verteilter Monolith”).
Schritt 4: Woran du erkennst, dass eine Grenze falsch ist
Eine gute Grenze hat hohe Kohäsion (cohesion: was zusammengehört, liegt zusammen) und geringe Kopplung (coupling: wenig Abhängigkeit nach außen). Eine falsche Grenze erkennst du an fünf Signalen:
- Zyklus: A importiert oder ruft B, und B ruft A.
- Gemeinsame Änderung (co-change): Fast jede Änderung an A erzwingt eine in B, im selben Release.
- Geschwätzigkeit (chattiness): Für einen einzigen Vorgang laufen viele Aufrufe zwischen den beiden hin und her.
- Geteilte Daten: Beide lesen und schreiben dieselbe Tabelle.
- Erzwungenes gemeinsames Deployment: Keiner lässt sich allein ausrollen.
Das erste Signal kannst du automatisch finden. Das Python-Modul ast liest Quelltext als Baum, ohne ihn auszuführen. So sieht ein Import darin aus:
Bei from a.b import c steckt das Modul in module='a.b', bei import a.b as x im alias.name. Der erste Namensteil vor dem Punkt ist das Top-Level-Modul. Damit baust du einen Abhängigkeitsgraphen für ein kleines Mietwagen-System und suchst Paare, die sich gegenseitig importieren:
buchung und flotte importieren sich gegenseitig: Signal 1. Ob daraus ein Fehler wird, hängt von den anderen Signalen ab. Die Reparatur ist meistens nicht “mehr Technik dazwischen” (eine Queue versteckt die Kopplung nur), sondern die Grenze neu zu ziehen: zusammenlegen, was zusammen ändert, oder die gemeinsame Abhängigkeit in ein drittes, einseitig genutztes Modul herausziehen.
Beachte: Die Funktion oben schaut nur auf tree.body, also auf Imports ganz oben in der Datei. Das ist eine Falle, auf die wir in Schritt 6 zurückkommen.
Schritt 5: Hexagonal an einem Beispiel: Abhängigkeiten zeigen nach innen
Bei Layered zeigt der Pfeil von oben nach unten, bis zur Datenbank. Das heißt: Deine Geschäftslogik hängt von der Datenbank ab. Hexagonal dreht das um (Dependency Inversion): Der Kern definiert, was er braucht (ein Port), und außen steckt ein Adapter, der es liefert. Das Beispiel versandkosten oben ist so gebaut: Der Kern verlangt eine Funktion tarif_quelle(land). Ob sie aus Postgres, einer API oder einem dict kommt, entscheidet der Aufrufer.
Gewinn: Du testest den Kern ohne Datenbank (das dict oben ist der Adapter im Test). Preis: eine zusätzliche Schnittstelle, die man pflegen muss. Bei einem Skript von 50 Zeilen ist das übertrieben. Bei Geschäftslogik, die Jahre lebt und sich oft ändert, zahlt es sich aus.
Schritt 6: Fitness Functions: Architekturregeln als Test
Eine Regel wie “die Geschäftslogik importiert nie die Datenbank” steht in jedem Architekturdokument und wird in jedem Projekt irgendwann verletzt, weil es keiner prüft. Eine Fitness Function macht aus der Regel einen automatischen Test, der in der CI läuft (so steht es in der Quelle unter “Leitplanken bauen” und in “Building Evolutionary Architectures”). Das Prinzip: Architektur ist nicht einmal entworfen, sondern wird bei jedem Commit gegen messbare Kriterien geprüft (evolutionäre Architektur, evolutionary architecture).
Ein erster Test mit der Funktion imports_von von oben. Regel: Das Modul preise darf datenbank und mailer nicht importieren.
Der zweite Quelltext verletzt die Regel, aber die Ausgabe ist trotzdem leer: Der Import steht in einer Funktion, und tree.body sieht nur die oberste Ebene. Ein Test, der eine Verletzung übersieht, ist schlimmer als gar keiner, weil er Sicherheit vortäuscht. ast.walk(baum) durchläuft dagegen alle Knoten, auch verschachtelte. Das brauchst du in Übung 5.
Schritt 7: Architecture Decision Records (ADR)
Ein ADR hält eine Architekturentscheidung kurz schriftlich fest, in der Regel als Datei im Repository neben dem Code. Er beantwortet in einem halben Jahr die Frage “Warum haben wir das so gebaut?”, wenn niemand sich mehr erinnert. Eine übliche Vorlage:
| Teil | Inhalt |
|---|---|
| Titel und Status | Kurzer Name, Status (vorgeschlagen, akzeptiert, ersetzt durch ADR-N) |
| Kontext | Welches Problem, welche Randbedingungen, welches Qualitätsszenario |
| Optionen | Mindestens zwei echte Alternativen, mit Vor- und Nachteilen |
| Entscheidung | Was wird gemacht, in einem Satz |
| Konsequenzen | Was wird leichter, was schwerer, was musst du jetzt zusätzlich tun |
| Annahmen, die die Entscheidung kippen | Woran du merkst, dass es Zeit ist, neu zu entscheiden |
Ein durchgespieltes Beispiel, mit einem anderen Fall als in Übung 3:
ADR 7: Produktsuche mit Postgres-Volltextsuche statt eigenem Suchdienst
Status: akzeptiert.
Kontext: Katalog mit rund 20 000 Produkten (Annahme des Beispiels), ein Team mit vier Personen. Szenario: Wenn ein Kunde im Shop sucht, kommt die Trefferliste bei 95 % der Anfragen in unter 400 ms.
Optionen: (1) Volltextsuche in der vorhandenen Postgres-Datenbank. Kein neuer Betrieb, Daten sind immer aktuell, aber begrenzte Funktionen bei Tippfehlern. (2) Eigener Suchdienst mit Synchronisation aus der Datenbank. Bessere Treffer, aber ein weiteres System, das betrieben und synchron gehalten werden muss.
Entscheidung: Option 1.
Konsequenzen: Kein zusätzlicher Betrieb. Ranking und Tippfehler-Toleranz bleiben einfach. Wir messen die Suchzeit als Metrik.
Kippt, wenn: der Katalog um ein Vielfaches wächst, die 400 ms nicht mehr gehalten werden oder Tippfehler-Toleranz zur Pflicht wird.
Der wichtigste Teil ist der letzte: Er sagt, unter welchen Annahmen die Entscheidung gilt. Dann ist es kein Dogma, sondern eine datierte Wette.
Falle
| Falle | Warum sie schmerzt |
|---|---|
| Microservices zu früh | Du zahlst Netzwerk, Betrieb und verteilte Daten, bevor du weißt, wo die Grenzen liegen. Falsche Grenzen sind hier am teuersten. |
| Verteilter Monolith | Services, die nur gemeinsam deployt werden können und sich gegenseitig synchron aufrufen. Alle Kosten von Microservices, kein Nutzen. |
| Schlagwort statt Szenario | “Skalierbar” lässt jede Architektur gut aussehen. Ohne Zahl kannst du nicht vergleichen. |
| Technik zwischen falsche Grenzen kleben | Eine Queue oder ein API-Gateway behebt keinen Zyklus. Die Kopplung bleibt, nur unsichtbarer. |
| Fitness Function, die Verstöße übersieht | Ein Architekturtest, der nur die oberste Ebene der Datei liest, meldet grün, während die Regel gebrochen ist. |
| ADR ohne Alternativen | Wenn nur eine Option dasteht, ist es eine Rechtfertigung, keine Entscheidung. |
Übungen
Übung 1: Drei Fälle, drei Stile (ca. 10 Min.)
Für jeden Fall gibt es genau eine Option, die zu den genannten Randbedingungen passt. Gehe die Bedingungen einzeln durch. Gib ein Tupel aus drei Buchstaben zurück, zum Beispiel ("A", "A", "A").
Fall 1. Ein Team mit 4 Entwicklern baut ein neues Produkt. Randbedingungen: (1) Die Anforderungen und das Fachmodell ändern sich wöchentlich, die Modulgrenzen sind unklar. (2) Niemand hat Erfahrung mit dem Betrieb vieler Dienste, es gibt kein Plattformteam. (3) Bestellung und Zahlung müssen in einer Datenbanktransaktion zusammen gespeichert werden. (4) Rund 300 Nutzer, ein Release pro Woche.
- A Pro Fachbereich ein eigener Service mit eigenem Repository, damit spätere Teams unabhängig deployen.
- B Ein modularer Monolith: klare Modulgrenzen, ein Repository, ein Deployment und eine gemeinsame Datenbank.
- C Fünf Services, die nur über Events auf einem Streaming-Log reden, damit sich Änderungen nicht berühren.
- D Eine Drei-Server-Aufteilung: UI-Server, Logik-Server und Datenbank-Server, getrennt betrieben und deployt.
Fall 2. Ein Onlinehändler hat 45 Entwickler in 7 Teams und einen Monolithen. Randbedingungen: (1) Die Produktsuche muss im Schlussverkauf 20-fach skalieren, das Rechnungswesen bleibt konstant. (2) Releases kommen nur noch monatlich, weil alle 7 Teams sich für ein gemeinsames Deployment abstimmen. (3) Die Fachgrenzen sind seit zwei Jahren stabil, Änderungen betreffen fast immer nur ein Team. (4) Ein Plattformteam betreibt bereits Container, CI/CD und Monitoring. (5) Neue Features müssen weiterlaufen, ein Umbau darf sie nicht wochenlang stoppen.
- A Alles an einem Wochenende in Microservices neu schreiben, damit ab Montag alle Teams unabhängig sind.
- B Alles bleibt ein Monolith. Für den Schlussverkauf kommt mehr Hardware, die Abstimmung zwischen Teams bleibt gleich.
- C Ein Event-Log als zentrale Wahrheit für alle Daten, damit die Teams sich nicht mehr abstimmen müssen.
- D Schrittweise entlang stabiler Grenzen Services herauslösen, die Suche zuerst, der Rest bleibt Monolith.
Fall 3. Ein Team mit 4 Entwicklern hat Buchung und Flotte als zwei Services betrieben. Beobachtungen: (1) In den letzten 10 Änderungen mussten 9 beide Services im selben Release ändern. (2) Eine Buchung löst 4 synchrone Aufrufe zwischen beiden aus. (3) Beide lesen und schreiben dieselbe Tabelle reservierung. (4) Keiner der beiden wurde je einzeln skaliert oder einzeln ausgerollt.
- A Beide zu einem Modul zusammenlegen, denn die Grenze trennt hier genau das, was zusammengehört.
- B Eine Message Queue dazwischen setzen, damit die Aufrufe asynchron werden und die Dienste entkoppeln.
- C Die Tabelle
reservierungin einen dritten Service auslagern, den beide Services aufrufen. - D Die API zwischen beiden versionieren und Contract Tests einführen, damit sie getrennt deployen können.
Frage bei jedem Fall: Welche Randbedingung spricht gegen die Option? Und: Welches Problem löst eine Option, das hier gar nicht besteht?
antwort = ("B", "D", "A")
antwortFall 1: Der modulare Monolith erfüllt alle vier Bedingungen: Grenzen sind noch verschiebbar (1), Betrieb bleibt einfach (2), die Transaktion bleibt eine Transaktion (3). Fall 2: Eine schrittweise Herauslösung nutzt die stabilen Grenzen (3), löst das Skalierungs- und Release-Problem (1, 2), das Plattformteam trägt den Betrieb (4) und der Betrieb der Features geht weiter (5). Fall 3: 9 von 10 gemeinsame Änderungen, 4 Aufrufe pro Buchung und eine gemeinsame Tabelle sind vier der fünf Signale für eine falsche Grenze (gemeinsame Änderung, Geschwätzigkeit, geteilte Tabelle, erzwungenes gemeinsames Deployment). Zusammenlegen behebt die Ursache.
Übung 2: Diagramme lesen (ca. 4 Min.)
Hier ist das Mietwagen-System aus Schritt 4 als C4-Container-Diagramm (C4: Context, Container, Component, Code, vier Zoomstufen eines Systems; hier die Ebene “Container”, also die deploybaren Teile) und als Sequenzdiagramm für eine Buchung. Mermaid hat auch eine eigene C4-Syntax (bitte prüfen, ob sie in deiner Mermaid-Version stabil ist). Hier zeichnen wir die Container mit einem normalen Flussdiagramm.
sequenceDiagram actor Kunde participant Web as Web-App participant B as Buchung participant F as Flotte Kunde->>Web: Auto buchen Web->>B: buchen(auto, zeitraum) B->>F: Ist das Auto frei? F->>B: Welche Buchungen gibt es? B-->>F: Liste der Buchungen F-->>B: frei: ja B-->>Web: bestätigt Web-->>Kunde: Bestätigung
a) Welche zwei Elemente im Container-Diagramm hängen gegenseitig voneinander ab?
- A Web-App und Buchung-Service
- B Buchung-Service und Zahlungsdienst
- C Web-App und Flotte-Service
- D Buchung-Service und Flotte-Service
b) Wie viele Nachrichten laufen im Sequenzdiagramm zwischen Buchung und Flotte (in beide Richtungen, Antworten mitgezählt)?
- A 2
- B 4
- C 6
- D 8
Gib ein Tupel (a, b) mit zwei Buchstaben zurück.
Teil a: Gehe die Pfeile im Container-Diagramm durch. Welche Pfeile gehören zu den Signalen für eine falsch gezogene Grenze aus Schritt 4? Teil b: Zähle nur die Pfeile, bei denen Buchung und Flotte die beiden Enden sind, und gehe die Pfeile der Reihe nach durch.
antwort = ("D", "B")
antwort- Buchung fragt Flotte nach Verfügbarkeit, und Flotte fragt Buchung nach gebuchten Zeiträumen: Das ist der Zyklus aus Schritt 4. b) Es sind vier Nachrichten: zwei Fragen und zwei Antworten. Für eine einzige Buchung sprechen die beiden Services viermal miteinander, das Signal “Geschwätzigkeit”.
Übung 3: Einen ADR schreiben (ca. 10 Min., ohne Check)
Ausgangslage (die Zahlen sind Annahmen dieser Aufgabe, keine Messwerte aus der Praxis): Ein Team mit 4 Entwicklern betreibt einen Python-Monolithen mit einer Postgres-Datenbank. Nutzer laden Fotos hoch, pro Foto werden drei Größen erzeugt. Das ist CPU-lastig. Nach einer Upload-Welle von 100 Fotos steigt die Antwortzeit normaler Seiten im Web-Prozess von 200 ms auf 4 s. Nutzer dürfen bis zu 2 Minuten auf die Vorschaubilder warten. Es gibt kein Plattformteam, und ein zweites Produkt, das Bilder braucht, ist nicht geplant.
Schreibe in dieser Reihenfolge:
- Ein Qualitätsszenario mit Quelle, Stimulus, Reaktion und Messgröße (mit Zahl).
- Einen ADR nach der Vorlage aus Schritt 7. Wähle zwischen drei Optionen: (1) Größen weiter im Web-Prozess erzeugen und stärkere Server kaufen, (2) die Größen von einem Worker-Prozess erzeugen lassen, der Aufträge aus einer Queue holt (gleicher Code, gleiches Repository), (3) einen eigenen Bildservice mit eigener Datenbank und eigener API bauen.
Qualitätsszenario. Quelle: Nutzer. Stimulus: laden in einer Minute insgesamt 100 Fotos hoch (Umgebung: normaler Betrieb). Reaktion: Das System nimmt die Uploads an und erzeugt die Größen im Hintergrund. Messgröße: Die Seitenaufrufe bleiben bei 95 % unter 500 ms, und alle Vorschaubilder sind nach spätestens 2 Minuten da.
ADR 12: Bildgrößen von einem Worker erzeugen
Status: akzeptiert.
Kontext: Die Größenerzeugung läuft im Web-Prozess und verdrängt normale Anfragen (200 ms auf 4 s bei 100 Uploads). Das Team besteht aus 4 Personen ohne Plattformteam. Nutzer tolerieren 2 Minuten Wartezeit auf Vorschaubilder.
Optionen:
- Im Web-Prozess lassen, stärkere Server: kein Umbau, aber jede Welle belastet die Seiten weiter, und du zahlst für Spitzenlast dauerhaft.
- Worker-Prozess mit Queue (gleiches Repository): trennt die Last vom Web-Prozess, ein Deployment-Artefakt mit zwei Startbefehlen, kein neuer Datenspeicher. Du musst Duplikate und Fehlversuche behandeln (idempotenter Konsument, Dead Letter Queue, Lektion 06).
- Eigener Bildservice: maximale Unabhängigkeit, aber ein weiteres System mit eigener Datenhaltung, API-Versionierung und Betrieb, ohne dass ein zweites Team oder Produkt ihn heute braucht.
Entscheidung: Option 2. Der Upload speichert das Original und legt einen Auftrag in die Queue. Der Web-Prozess antwortet sofort.
Konsequenzen: Seiten bleiben schnell. Vorschaubilder erscheinen mit Verzögerung, die UI braucht einen Zustand “wird verarbeitet”. Der Worker muss idempotent sein (gleiche Datei zweimal verarbeiten darf nichts kaputt machen). Wir überwachen die Queue-Länge und das Alter des ältesten Auftrags.
Kippt, wenn: ein weiteres Team oder Produkt die Bildverarbeitung braucht, wenn die Last ein eigenes Skalieren erfordert, das der gemeinsame Worker nicht liefert, oder wenn das Team wächst und eigene Releases braucht.
Selbstcheck.
Übung 4: Ein Sequenzdiagramm zeichnen (ca. 6 Min., ohne Check)
Zeichne in Mermaid (sequenceDiagram) den Ablauf der Entscheidung aus Übung 3. Teilnehmer: Nutzer, Web-App, Queue, Worker, Speicher. Anforderungen: (1) Die Web-App antwortet auf den Upload sofort mit “angenommen”. (2) Das Original wird gespeichert, ein Auftrag kommt in die Queue. (3) Der Worker holt den Auftrag, erzeugt die Größen, legt sie in den Speicher und bestätigt erst danach (Ack). (4) Später fragt der Nutzer den Status ab und bekommt die Vorschaubilder. Du kannst deinen Entwurf im Editor von https://mermaid.live ansehen.
sequenceDiagram
actor Nutzer
participant Web as Web-App
participant Q as Queue
participant W as Worker
participant S as Speicher
Nutzer->>Web: Foto hochladen
Web->>S: Original speichern
Web->>Q: Auftrag einreihen
Web-->>Nutzer: angenommen
W->>Q: Auftrag holen
W->>S: drei Größen speichern
W->>Q: Ack nach dem Speichern
Nutzer->>Web: Status abfragen
Web->>S: Vorschaubilder lesen
Web-->>Nutzer: Vorschaubilder
Selbstcheck.
Übung 5: Eine Fitness Function schreiben (ca. 10 Min.)
Dein Projekt hat die Schichten ui, service, repository (von oben nach unten). Regel: Ein Modul darf nur Module der eigenen oder einer tieferen Schicht importieren, nie eine höhere. repository darf also service nicht importieren, service darf ui nicht importieren, aber ui darf service und repository importieren.
Schreibe verstoesse(projekt, schichten):
projektist eindictvon Modulname zu Quelltext,schichteneine Liste der Modulnamen von oben nach unten.- Sie gibt eine sortierte Liste von Tupeln
(modul, importiert)zurück, ohne Duplikate. - Nur Imports von Modulen aus
schichtenzählen.import os,import sqlite3und Ähnliches werden ignoriert. - Der erste Namensteil zählt:
import ui.widgets as wundfrom service.helfer import hmeinen die Moduleuiundservice. - Imports zählen überall im Quelltext, auch innerhalb von Funktionen und Klassen.
- Relative Imports ohne Modulnamen (
from . import x) werden ignoriert, dürfen aber nicht abstürzen.
Gib jeder Schicht eine Zahl, die ihre Lage in der Liste beschreibt. Wie erkennst du über diese Zahlen, dass ein Import “nach oben” zeigt? Für “überall” brauchst du einen Durchlauf über alle Knoten des Baums, nicht nur über die oberste Ebene. Bei from ... import kann der Modulname fehlen.
import ast
def verstoesse(projekt, schichten):
rang = {name: i for i, name in enumerate(schichten)}
gefunden = set()
for modul, quelltext in projekt.items():
for knoten in ast.walk(ast.parse(quelltext)):
ziele = []
if isinstance(knoten, ast.Import):
ziele = [a.name.split(".")[0] for a in knoten.names]
elif isinstance(knoten, ast.ImportFrom) and knoten.module:
ziele = [knoten.module.split(".")[0]]
for ziel in ziele:
if ziel in rang and rang[ziel] < rang[modul]:
gefunden.add((modul, ziel))
return sorted(gefunden)
print(verstoesse({"ui": "import service\n", "service": "import ui\n"}, ["ui", "service"]))
verstoesseast.walk findet auch Imports in Funktionen. Der Rang vergleicht die Schichten, set entfernt Duplikate, sorted macht die Ausgabe stabil, und der Check auf knoten.module fängt from . import x ab.
Merksatz
Es gibt keine beste Architektur, nur Entscheidungen mit Kosten: Wähle den einfachsten Stil, der deine messbaren Qualitätsszenarien erfüllt (meist ein modularer Monolith, bis stabile Grenzen, mehrere Teams und unterschiedliche Skalierung etwas anderes verlangen), halte die Entscheidung in einem ADR mit den Annahmen fest, die sie kippen würden, und schütze die Regeln mit Fitness Functions.
Prüfstein
Wann ist ein Monolith die bessere Wahl als Microservices, und woran erkennst du, dass eine Grenze zwischen Modulen falsch gezogen ist?
Quelle: quellen/konzeptuebersicht-software-grundlagen.md, Abschnitt “6. Design und Architektur” (Grundprinzipien Coupling und Cohesion, Architekturstile Layered, Hexagonal/Clean, Event-driven, modularer Monolith, Microservices, Entscheidungsdokumentation: Qualitätsattribute und Architecture Decision Records, Prüfstein Monolith gegen Microservices) und Abschnitt “Leitplanken bauen” (Architekturtests als Fitness Functions); quellen/konzeptuebersicht-software-fortgeschritten.docx, Schicht 6 “Architektur und Systemdesign” (Qualitätsattribute: Szenarien statt Schlagworte, Trade-off-Analyse ATAM, Fitness Functions, evolutionäre Architektur; Organisation und Architektur: Conway’s Law; Integrationsmuster).
Über die Quelle hinaus (allgemeines Fachwissen, die Quellen sind Stichwortlisten): die vier Teile eines Qualitätsattributs-Szenarios, die Beschreibung der fünf Stile und ihrer Kosten, die Rechnung zur seriellen Verfügbarkeit (ein Modell mit unabhängigen Ausfällen, keine Messung), die fünf Signale für falsch gezogene Grenzen, die Vorlage und der Aufbau eines ADR, die Beschreibung der C4-Ebenen und die Aussage, dass Mermaid eine C4-Syntax hat (bitte prüfen). Alle Zahlen in den Fallstudien und im ADR sind erfundene Annahmen der Aufgaben, keine Erfahrungswerte. Der Hinweis auf ein ESLint-Plugin für Modulgrenzen ist bitte zu prüfen.