Pattern-Katalog

Jedes hier verwendete Entwurfsmuster mit Fundstelle im Code, kurzer Erklärung und Begründung "warum hier". Wird mit jeder Phase ergänzt - diese Version deckt den finalen Stand nach Phase 8 ab (alle 9 Spring-Boot-Module + das Angular-Frontend + Container/Kubernetes/CI- Infrastruktur + E2E/Lasttest-Vertiefung).

Pattern Kategorie Fundstelle Warum hier verwendet
Aggregate Root DDD (taktisch) AggregateRoot.java Erzwingt, dass fachliche Invarianten nicht umgangen werden können, indem nur das Root von außen erreichbar ist.
Value Object DDD (taktisch) Isbn.java, Money.java, EmailAddress.java "Make illegal states unrepresentable" - ein Isbn-Objekt kann nie eine ungültige ISBN enthalten, die Prüfung passiert genau einmal im Konstruktor statt an jeder Verwendungsstelle.
Domain Event DDD (taktisch) DomainEvent.java Trennt "was ist im Aggregate fachlich passiert" (intern) von "was wird nach außen kommuniziert" (Integrationsevent, siehe common-events) - bewusst zwei verschiedene Modelle.
Custom Exception Hierarchy Fehlerbehandlung DomainException.java Technische von fachlichen Fehlern trennen; ermöglicht zentrales Mapping auf HTTP-Statuscodes (siehe RFC-7807-Handler unten).
Problem Details / RFC 7807 API-Design GlobalExceptionHandler.java Standardisiertes, maschinenlesbares Fehlerformat statt projektspezifischem Ad-hoc-JSON; von Spring Boot 3 nativ unterstützt (ProblemDetail).
Chain of Responsibility (technisch: Servlet Filter) Querschnittsbelang CorrelationIdFilter.java Jeder Request durchläuft eine Kette von Filtern; hier zur Anreicherung des Log-Kontexts, unabhängig von der eigentlichen fachlichen Verarbeitung.
Auto-Configuration-Modul ("Mini-Starter") Modularität CommonWebAutoConfiguration.java, CommonSecurityAutoConfiguration.java Macht ein internes Modul zu einem "Plug-and-Play"-Baustein, unabhängig vom Hauptklassen-Package des einbindenden Service - dasselbe Muster wie offizielle Spring-Boot-Starter.
Sensible Default, Override via @ConditionalOnMissingBean Erweiterbarkeit CommonSecurityAutoConfiguration.java Bibliothekscode erzwingt keine starre Vorgabe, sondern liefert einen überschreibbaren Standard - jeder Service kann eigene Sicherheitsregeln definieren, wo nötig.
Adapter (Claim-Mapping) Integration KeycloakRealmRoleConverter.java Übersetzt Keycloaks proprietäres Token-Format (realm_access.roles) auf Spring Securitys Standard-GrantedAuthority-Schnittstelle.
Constants-Registry statt String-Literalen Wartbarkeit LibraryTopics.java, LibraryQueues.java, LibraryRoles.java Verhindert Tippfehler-Divergenz zwischen Producer/Consumer bzw. zwischen Token-Rolle und @PreAuthorize-Check - der Compiler findet den Fehler statt eines Laufzeit-403.
Consumer-Driven Schema Versioning Integration IntegrationEvent.schemaVersion() Leichtgewichtige Alternative zu einer zentralen Schema-Registry (Trade-off dokumentiert in ADR-0003 bzw. common-events/pom.xml).
Singleton Container Pattern (Testcontainers) Testing LibraryTestContainers.java withReuse(true) vermeidet, dass jeder Testlauf Container komplett neu hochfährt - spürbarer Geschwindigkeitsgewinn in einem Multi-Module-Reactor mit vielen Integrationstests.
Spezifischster-Handler-gewinnt (mehrere @RestControllerAdvice) Fehlerbehandlung SecurityExceptionHandler.java Realer Bug, gefunden beim Testen (Phase 2): AccessDeniedException wurde vom generischen Exception-Handler in common-web als HTTP 500 statt 403 beantwortet. Spring waehlt automatisch den spezifischsten @ExceptionHandler ueber ALLE Advice-Beans hinweg - die Lehre daraus steht in der Teststrategie.
Fitness Function (ArchUnit) Architektur-Governance architecture-tests/ Architekturregeln (z. B. "common-domain bleibt framework-frei", "keine Feld-Injection") werden automatisiert bei jedem Build geprüft statt nur im Code-Review erinnert.
Constructor Injection Dependency Injection erzwungen durch ConstructorInjectionTest.java Sichtbare, testbare, unveränderliche (final) Abhängigkeiten statt versteckter Feld-Injection.
Layered Architecture (Fitness Function) Architektur-Governance LayeredArchitectureTest.java Erzwingt die Schichtenfolge api → application → domain ← infrastructure je Service - Verstöße lassen den Build fehlschlagen statt erst im Review aufzufallen.
Strategy (via Enum-Konstanten) GoF-Verhalten MembershipTier.java implements LendingPolicy.java Tarifabhängige Ausleihregeln ohne if/else-Kaskade; Enum-Strategie statt Klassenhierarchie, da die Tarifmenge fachlich geschlossen ist (siehe Klassen-Javadoc für die Abwägung gegenüber separaten Strategy-Klassen).
Strongly Typed ID DDD (taktisch) MemberId.java, BookId.java, CopyId.java Verhindert, dass zwei UUID-Parameter in einer Methodensignatur versehentlich vertauscht werden - der Compiler unterscheidet die Typen.
Aggregate-lokale Entity + öffentlicher Snapshot-Typ DDD (taktisch) Copy.java, CopySnapshot.java Copy hat einen paketprivaten Konstruktor - die Infrastruktur-Schicht darf ein Aggregat nur über Book.reconstitute(..., List<CopySnapshot>) wiederherstellen, nie einzelne Kindobjekte direkt erzeugen. Erzwingt die Aggregate-Grenze auf Compiler-Ebene statt nur per Konvention.
Specification Pattern (Spring Data JPA) Persistenz BookSpecifications.java Kombinierbare, unabhängig erweiterbare Suchkriterien statt einer Repository-Methode mit wachsender Parameterliste (findByTitleAndAuthorAndCategoryAnd...).
Cache-Aside / Read-Through Performance BookQueryService.java, CacheConfig.java @Cacheable/@CacheEvict gegen Redis für den häufigen "Buch per Id"-Zugriff; die (parameterreichere, seltener wiederholte) Suche wird bewusst NICHT gecacht.
Domain Event → Integrationsevent-Übersetzung nach Commit Event-Driven Architecture CatalogEventPublisher.java @TransactionalEventListener(phase = AFTER_COMMIT) verhindert, dass ein Event veröffentlicht wird, dessen DB-Änderung durch einen Rollback nie stattfand. Leichtgewichtiger Kompromiss gegenüber echtem Transactional Outbox (lending-service) - Trade-off dokumentiert in CopyMarkedAvailableEvent.java.
Saga (Orchestrierung) mit Kompensation Verteilte Transaktionen BorrowBookService.java Koordiniert catalog-service (Exemplar reservieren) + member-service (Berechtigung) + lokale DB - schlägt der letzte Schritt fehl, wird die bereits erfolgte Exemplar-Reservierung kompensiert. Siehe ADR-0008.
Transactional Outbox Verteilte Transaktionen OutboxEntry.java, OutboxEventListener.java, OutboxPublisher.java Loan-Änderung und Event-Eintrag committen atomar in derselben Transaktion (synchroner @EventListener, NICHT AFTER_COMMIT); ein separater Poller veröffentlicht später an Kafka. Stärkere Garantie als catalog-service's Ansatz - Vergleich in ADR-0008.
Circuit Breaker + Retry (Resilience4j) Resilience CatalogServiceAdapter.java @Retry überbrückt kurze Aussetzer, @CircuitBreaker verhindert bei anhaltendem Ausfall weiteres Warten ("Fail Fast") - mit fachlicher Fallback-Exception statt technischem Fehler.
Anti-Corruption Layer Integration CatalogBookView.java, CatalogServiceAdapter.java lending-service übersetzt catalog-services externes JSON-Format in eigene, schlanke Domain-Typen - das externe Format "leakt" nur in der Adapter-Klasse, nirgendwo sonst.
Token Relay Security TokenRelayFeignInterceptor.java Reicht das JWT des aufrufenden Nutzers unverändert an nachgelagerte Services weiter, statt sich erneut zu authentifizieren.
Self-Invocation-Falle vermeiden (separates Transactional-Bean) Spring/AOP LoanWriteService.java @Transactional auf einer Methode, die von derselben Klasse intern aufgerufen wird, wird von Spring stillschweigend IGNORIERT (Proxy-basierte AOP faengt nur externe Aufrufe ab) - deshalb ein eigenes Bean statt einer privaten Methode.
Event-Choreographie Event-Driven Architecture FulfillReservationService.java reservation-service reagiert eigenständig auf catalog-services CopyAvailableEvent, ohne dass ein Orchestrator es dazu auffordert - Kontrast zu lending-service's Saga (Orchestrierung). Beide Stile im selben Projekt zu sehen, macht den Unterschied greifbar.
Idempotent Consumer (Upsert-Variante) Messaging Fine.java#recalculate, HandleLoanOverdueService.java Bei erneuter Zustellung desselben LoanOverdueEvent (z. B. weil der tägliche Scan die Überfälligkeit erneut feststellt) wird die bestehende Gebühr aktualisiert statt eine zweite angelegt.
Idempotent Consumer (Dedup-Variante) Messaging ProcessNotificationCommandService.java commandId als Idempotenzschlüssel: eine bereits verarbeitete Nachricht (RabbitMQ "at-least-once") wird beim zweiten Empfang übersprungen, statt ein zweites Mal "gesendet" zu werden.
Retry + Dead-Letter-Queue Messaging/Resilience RabbitConfig.java Bis zu 3 lokale Wiederholversuche mit Backoff, bevor eine Nachricht (ohne Requeue) an die konfigurierte Dead-Letter-Queue weitergeleitet wird - verhindert einen endlosen "Poison-Message"-Zustellungs-Loop.
Consumer-Driven Contract Testing Testing/Integration CatalogServiceContractTest.java (Konsument), CatalogServiceProviderContractIT.java (Provider) Prüft, ob Producer und Consumer eines REST-Aufrufs bzw. Kafka-Events kompatibel bleiben, ohne beide gemeinsam laufen lassen zu müssen (Definition siehe Glossar) - deckt alle 8 echten Service-zu-Service-Paare ab, siehe teststrategie.md.
Treibender vs. getriebener Adapter Architektur-Governance LayeredArchitectureTest.java Ein Message-Listener (Infrastructure-Paket) ist architektonisch ein TREIBENDER Adapter (wie ein Controller) und darf application aufrufen; ein Repository-Adapter ist GETRIEBEN (wird von application über ein Domain-Interface aufgerufen). Erkenntnis aus einem echten ArchUnit-Regelverstoß bei reservation-/fine-/notification-service.
API Gateway Integration SecurityConfig.java, application.yml Single Entry Point: Routing per Pfad-Präfix zu den Fachservices, zentrale JWT-Prüfung, CORS für das Angular-Frontend - Clients kennen nur eine Adresse, nicht sechs.
Client-Side Service Discovery Integration discovery-server, eureka.client-Block in jeder Service-application.yml, CatalogFeignClient.java Jeder Fachservice registriert sich bei Eureka (sichtbar im Dashboard unter :8761) UND die tatsächliche Adressauflösung nutzt das inzwischen auch: @FeignClient(..., url = "${...:}") mit leerem Default lässt Feign bei fehlender expliziter URL auf Eureka-/spring-cloud-starter-loadbalancer-basierte Namensauflösung zurückfallen - aktiv in der vollcontainerisierten Compose-Umgebung, siehe ADR-0007 ("Nachtrag").
Zentrale Konfiguration (Config Server) Infrastruktur config-repo/application.yml Zwei Einstellungen, die in Phase 1-4 wortwörtlich in jedem Service dupliziert waren (logging.pattern.level, management.endpoint.health.show-details), liegen jetzt an einer Stelle. optional:configserver:... lässt jeden Service auch ohne laufenden Config Server starten (mit lokalen Defaults).
Signals-basierter State (Standard-Stil) Frontend-Zustandsverwaltung catalog.service.ts, fines.service.ts, members-admin.service.ts @Injectable({providedIn: 'root'})-Service haelt signal<T>()-Zustand direkt; Komponenten lesen ihn ohne async-Pipe oder manuelles Un-/Subscriben. Bewusster Standard-Stil dieser Anwendung - schlanker als NgRx fuer Zustand, der nicht global/Feature-uebergreifend geteilt werden muss.
NgRx (Redux-Pattern) als bewusster Kontrast Frontend-Zustandsverwaltung catalog-admin/store/ (catalog-admin.actions.ts, .reducer.ts, .effects.ts, .selectors.ts) Einziges Feature mit NgRx statt Signals - absichtlich, um Store/Reducer/Effects/Selectors direkt neben dem Signals-Stil zu zeigen. createActionGroup (moderne NgRx-API) statt einzelner createAction-Aufrufe; Side Effects (HTTP) leben ausschließlich in CatalogAdminEffects, der Reducer bleibt eine reine Funktion.
Functional Route Guard Frontend/Routing role.guard.ts Angular-15+-Stil: einfache Funktion statt Guard-Klasse mit CanActivate-Interface - weniger Boilerplate, direkt mit inject() nutzbar.
Functional HTTP Interceptor / Token Relay (Client-Seite) Frontend/Security auth.interceptor.ts Haengt das Access-Token automatisch an jeden Request an API_BASE_URL - Pendant zum serverseitigen Token-Relay-Pattern in lending-service (dort JWT von Service zu Service, hier von Browser zu Gateway).
RFC-7807-Fehlerparsing (Frontend-Spiegel) Frontend/Fehlerbehandlung z. B. fines.service.ts#extractErrorMessage Liest err.error.detail aus der ProblemDetail-Antwort des Backends (siehe GlobalExceptionHandler) - dieselbe Fehlerkontrakt-Annahme auf beiden Seiten der Anwendung, keine eigene Frontend-Fehlerkonvention.
Authorization Code Flow + PKCE Security auth.config.ts, auth.service.ts angular-oauth2-oidc mit initCodeFlow() - der für Single-Page-Apps empfohlene OAuth2-Flow (kein Client-Secret im Browser, PKCE schützt den Code-Austausch), Pendant zum Resource-Server-Setup im Backend (ADR-0005).
Multi-Stage-Container-Build Deployment library-platform/Dockerfile Ein generisches Dockerfile (von podman build gelesen) fuer alle neun Services (parametrisiert per --build-arg MODULE): Stage 1 baut mit vollem Maven+JDK-Image, Stage 2 kopiert NUR das fertige Jar in ein schlankes JRE-Image - vermeidet Compiler/Build-Tools im Produktions-Image.
Classpath-getriebene Auto-Configuration (reines Abhaengigkeits-Modul) Modularität common-observability Anders als common-web/common-security bringt dieses Modul KEINE eigene @AutoConfiguration-Klasse mit - Spring Boot erkennt Micrometer/Zipkin-Bibliotheken automatisch allein an ihrer Anwesenheit auf dem Klassenpfad. Bewusster Kontrast im selben Projekt: zwei verschiedene Wege, wie ein "common"-Modul Verhalten hinzufügt.
Zwei Adressen fuer denselben Dienst (Issuer vs. JWKS-Endpunkt) Security api-gateway/application.yml issuer-uri muss zum iss-Claim im Token passen (das sieht der Browser, der sich ueber localhost:8180 einloggt), jwk-set-uri wird dagegen vom Backend-Container selbst aufgerufen (keycloak:8080, containerinternes Netzwerk) - zwei unterschiedliche Hostnamen fuer denselben Keycloak, je nachdem WER die Anfrage stellt. Ausfuehrlich erklaert in docker-compose-guide.md.
Dual-Profile-Deployment (Compose vs. Kubernetes) Architektur-Governance on-profile: k8s-Bloecke in acht application.yml-Dateien, siehe auch ADR-0007 Derselbe Anwendungscode laeuft mit unveraendertem Jar in zwei grundverschiedenen Deployment-Umgebungen - der einzige Code-seitige Unterschied ist ein aktiviertes Spring-Profil (SPRING_PROFILES_ACTIVE=k8s), alles andere (Hostnamen) kommt ueber Umgebungsvariablen.
Generisches Helm-Chart + service-spezifische values-Dateien Deployment infra/helm/library-service, infra/helm/values/ EIN Chart fuer acht strukturell identische (zustandslose Deployment+Service+optional HPA/Ingress), aber unterschiedlich konfigurierte Services - vermeidet, dieselben Kubernetes-Ressourcen-Templates achtmal zu duplizieren.
Stabile Selector-Labels getrennt von vollen Labels Deployment _helpers.tpl library-service.selectorLabels ist eine bewusst kleinere, ZEITLICH STABILE Teilmenge von library-service.labels - ein Deployment-Selector darf sich nach dem ersten Rollout nie mehr aendern, waehrend z. B. die Chart-Version in den vollen Labels bei jedem Release wechseln kann.
Matrix-Build (CI) CI/CD .github/workflows/ci.yml Ein Job-Template (container-build, podman build), einmal parallel fuer alle neun Services ausgefuehrt (strategy.matrix) statt neun fast identischer Kopier-Jobs.
Environment-Gated Promotion CI/CD Jenkinsfile Automatischer Durchlauf bis zur QS-Stufe, danach ein manuelles input-Freigabe-Gate vor Produktion - verhindert, dass jeder erfolgreiche Build automatisch live geht, ohne jede Stufe einzeln von Hand anzustossen.
Values-Layering statt Datei-Duplizierung (Helm) Deployment infra/helm/values/env/ Zwei kleine, umgebungsWEITE Overlay-Dateien (test-values.yaml, qs-values.yaml) werden als zweite -f hinter der service-spezifischen Datei uebergeben, statt alle 8 Service-Dateien pro Umgebung zu duplizieren (8x3=24 Dateien).
Map statt Liste fuer overlay-faehige Helm-Werte Deployment library-service/values.yaml, deployment.yaml, env/kind-values.yaml env ist bewusst eine Map ({SPRING_X: "Y"}) statt einer Liste ([{name: X, value: Y}]) - Helm merged zwischen mehreren -f-Dateien nur MAPS schluesselweise, Listen werden bei der letzten -f-Datei komplett ERSETZT. Erst dadurch kann ein gemeinsames kind-Overlay (siehe kind-cluster.md) einzelne Umgebungsvariablen ueberschreiben, ohne den kompletten env-Block jedes der 8 Services zu duplizieren.
Label-basiertes Service-Discovery (Reverse-Proxy) Deployment/Infrastruktur infra/docker-compose.yml (Traefik-Labels je Service), shared/enterprise-infrastructure/docker-compose.yml Traefik erkennt Dienste automatisch anhand von Container-Labels (traefik.http.routers..., via Podmans Docker-kompatible API) statt einer manuell gepflegten, separaten Proxy-Konfigurationsdatei - ein neuer Dienst mit Hostnamen-Wunsch bekommt einfach vier zusaetzliche Zeilen labels:, keine zweite Datei zu pflegen. Funktioniert projektuebergreifend ueber ein gemeinsames EXTERNES Container-Netzwerk (proxy), siehe reverse-proxy.md.
Automatisierte PR-Checkliste (ArchUnit) Architektur-Governance NoJpaEntityInApiTest.java Ein bisher rein manuell im Review geprüfter Punkt ("REST-DTOs sind keine JPA-Entities") wird zum automatisierten Build-Fehler statt eines im Review leicht übersehenen Details.
Slice-Zyklenfreiheit (ArchUnit) Architektur-Governance NoCyclicDependenciesTest.java Ergänzt die Schichten-Regel (erlaubte RICHTUNG zwischen Schichten) um Zyklenfreiheit ZWISCHEN den Unterpaketen jedes Service - zwei Pakete derselben Schicht dürften sich laut Schichten-Regel gegenseitig referenzieren, ein Zyklus bleibt trotzdem ein Wartbarkeits-Alarmsignal.

Stolperstein: MapStruct erkennt fluent-benannte Accessoren NICHT automatisch

Der erste vollständige E2E-Testlauf gegen den echten Stack (siehe library-frontend/e2e/README.md#gefundene-bugs) deckte einen Bug auf, der fast jede API-Antwort im gesamten Backend betraf: BookDtoMapper, FineDtoMapper, LoanDtoMapper, MemberDtoMapper und ReservationDtoMapper gingen (ein MemberDtoMapper-Kommentar sagte das sogar explizit, aber FALSCH) davon aus, MapStruct erkenne fluent-benannte Accessor-Methoden (title(), status() - bewusst ohne get-Präfix, siehe die reichen Domainmodelle) automatisch, "wie bei einem Record".

Das stimmt nicht: MapStructs automatische Feld-zu-Feld-Zuordnung erkennt nur JavaBean-Getter (getTitle()) ODER echte record-Typen (per Reflection auf deren canonical constructor) - eine normale Klasse mit fluent-benannten Methoden fällt durch beide Raster. Ohne explizite @Mapping(target = "...", expression = "java(...)") wird ein "passend benanntes" Zielfeld dann still auf seinen Default-Wert gesetzt (null bei Objekten, 0 bei int) - nur eine Build-Warnung, kein Fehler. Betraf zuletzt u. a. BookResponse.title/publisher, MemberResponse.firstName/lastName/tier/status/registeredAt, LoanResponse.borrowedAt/dueDate/status und ReservationResponse.queuePosition/status.

Lehre: bei MapStruct + reichem Domainmodell (fluent Accessoren) gilt die Regel "JEDES Zielfeld bekommt eine explizite @Mapping", nicht nur die Felder, die offensichtlich eine Transformation brauchen (Value-Object-Entpacken o. ä.) - genau die scheinbar "einfachen" Felder sind hier das Risiko, weil sie am ehesten fälschlich als "wird schon automatisch gemappt" durchgehen. Ein unmappedTargetPolicy = ReportingPolicy.ERROR auf Reactor-Ebene hätte das schon beim Build gefangen statt erst im Browser - eine dokumentierte Vertiefungsaufgabe für dieses Projekt.

Noch nicht implementiert (bewusste Vertiefungsaufgaben)

Alle acht geplanten Phasen sind umgesetzt. Ein paar bewusst offen gelassene Vertiefungsaufgaben bleiben als Lernübung bestehen (jeweils an Ort und Stelle im Code/ADR dokumentiert, keine vergessenen Punkte):

⌂ Cockpit