Pattern-Katalog

Verwendete Entwurfsmuster in diesem Projekt, jeweils mit Fundstelle und Begründung. Wird mit jeder Phase ergänzt (siehe Lernpfad).

Value Object

Was: unveränderliches Objekt ohne Identität, definiert durch seine Werte; erzwingt seine eigenen Invarianten im Konstruktor/in der Fabrikmethode.

Wo: common-domain/.../Money.javaMoney.of(...) lehnt negative Beträge ab, Money.add(...) lehnt Währungs-Mismatches ab. Als Java record implementiert (strukturelle Gleichheit, Unveränderlichkeit "for free").

Warum: verhindert, dass ein ungültiger Geldbetrag überhaupt erst instanziiert werden kann ("Make Invalid States Unrepresentable"). Siehe TDD-Katalog für die Commit-Historie der Entstehung per TDD.

Aggregate (DDD)

Was: Konsistenzgrenze mit eigener Identität, sammelt Domain Events statt sie direkt zu publizieren.

Wo: common-domain/.../AggregateRoot.java — Basisklasse für alle fachlichen Aggregate (Account, Customer, Card, ... ab den jeweiligen Phasen).

Warum: trennt "wann passiert etwas fachlich Bedeutsames" (Aggregate-Methode) von "wann wird es nach außen sichtbar" (pullDomainEvents(), aufgerufen von der Anwendungsschicht nach erfolgreicher Persistenz) — siehe Architektur-Überblick.

Repository-Port (Dependency Inversion)

Was: von der Domain-/Anwendungsschicht definiertes Interface für Persistenz, implementiert von der Infrastrukturschicht.

Wo: ab Phase 1 in jedem Domain-Service (domain.<Aggregat>Repository Interface, infrastructure.persistence.Jpa<Aggregat>RepositoryAdapter Implementierung).

Warum: siehe ADR-0002. Ermöglicht Anwendungsfall-Tests ohne Datenbank.

Domain Event / Integration Event

Was/Wo: common-domain/.../DomainEvent.java (modul-intern) vs. common-events/.../IntegrationEvent.java (Service-übergreifender, stabiler Vertrag), siehe CustomerRegisteredEvent.

Warum: entkoppelt den internen Domain-Ereignistyp eines Aggregats von dem, was tatsächlich "nach draußen" auf Kafka veröffentlicht wird — eine interne Refaktorierung des Aggregats muss nicht zwangsläufig den öffentlichen Event-Vertrag brechen.

Saga mit Kompensation

Was: Koordination mehrerer lokaler Transaktionen über Service-Grenzen (Abbuchung in accounts-service → Gutschrift in accounts-service → Ledger-Buchung in payments-service, mit explizitem Kompensationsschritt bei Fehlschlag der Gutschrift).

Wo: payments-service, InitiateTransferUseCase.

Warum: siehe ADR-0005.

Zustandsloser Domain-Service

Was: Fachliche Logik, die zu keinem einzelnen Aggregate gehört (weil sie mehrere Werte kombiniert oder reine Berechnung ohne eigenen Zustand ist), als eigene, injizierbare Klasse statt als statische Utility-Methode oder aufgeblähte Aggregate-Methode.

Wo: RiskClassifier (customer-service), IbanGenerator/PanGenerator (accounts-/cards-service), LedgerBooking (payments-service), CreditScorer/ RepaymentScheduleGenerator (lending-service).

Warum: haelt Aggregate schlank und fokussiert auf ihre eigenen Invarianten; ein Domain-Service ist ohne Infrastruktur (keine Spring-/JPA-Abhaengigkeit) isoliert testbar - genau das macht ihn zum idealen Ziel fuer Triangulation ueber mehrere Testfaelle (siehe TDD für Entwickler).

Transactional Outbox

Was: eine ausgehende Nachricht wird nicht direkt an den Message-Broker gesendet, sondern in derselben Datenbank-Transaktion wie die fachliche Änderung als Zeile in einer eigenen "Outbox"-Tabelle gespeichert. Ein separater Prozess (Relay/Poller) liest unveröffentlichte Zeilen und sendet sie asynchron, danach markiert er sie als veröffentlicht.

Wo: customer-service, RegisterCustomerUseCase (schreibt OutboxEvent) + OutboxRelay (@Scheduled, liest und veröffentlicht).

Warum: löst das "Dual-Write-Problem" - ohne Outbox sind "Kunde speichern" und "Event veröffentlichen" zwei getrennte, nicht atomare Schritte; ein Absturz dazwischen verliert das Event stillschweigend (in diesem Projekt bis Phase 7 tatsächlich so vorhanden, siehe ADR-0007, Nachtrag Outbox). Die Outbox-Tabelle lebt in derselben Datenbank wie die fachlichen Tabellen, wird also von derselben Transaktion mit abgesichert - "genau einmal speichern, mindestens einmal veröffentlichen" statt "vielleicht gar nicht veröffentlichen".

Message-Driven Adapter (Kafka-Konsument)

Was: Ein duenner Adapter, der ein eingehendes Integrationsevent unveraendert an einen Anwendungsfall weiterreicht - keine fachliche Logik im Adapter selbst.

Wo: notification-service, infrastructure.messaging (CustomerEventsListener, LoanEventsListener, PaymentEventsListener). LoanEventsListener zeigt zusaetzlich einen @KafkaListener mit mehreren @KafkaHandler-Methoden fuer verschiedene Event-Typen auf demselben Topic.

Warum: symmetrisch zum Repository-Port/Adapter-Muster oben, nur fuer eingehende statt ausgehende Infrastruktur - der Adapter selbst wird ohne echten Broker getestet (nur die Weiterleitung), das tatsaechliche Verhalten steckt vollstaendig im Anwendungsfall.

⌂ Cockpit