feat: integrate new desktop shell and extend backend & ADRs
- Added `meldestelle-desktop` module using JVM/Compose Desktop, registered in `settings.gradle.kts`. - Integrated new screens and desktop navigation into core: `Veranstaltungen`, `TurnierDetail`, etc. - Expanded backend with `ExposedFunktionaerRepository` in `officials-infrastructure`. - Completed ADRs for bounded context mapping (`ADR-0014`) and context map (`ADR-0015`). - Updated and extended project documentation with session logs and architecture decisions. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
---
|
||||
type: ADR
|
||||
id: ADR-0014
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-24
|
||||
---
|
||||
|
||||
# ADR-0014: Bounded Context Mapping (SCS-Architektur)
|
||||
|
||||
## Status
|
||||
|
||||
Akzeptiert
|
||||
|
||||
## Kontext
|
||||
|
||||
Mit der Entscheidung für Domain-Driven Design (→ ADR-0002) und der modularen Architektur (→ ADR-0001) war es
|
||||
notwendig, die fachlichen Grenzen des Systems explizit zu definieren. Die ursprünglichen Module (`masterdata`,
|
||||
`members`, `horses`, `events`) spiegelten technische Kategorien wider, nicht die tatsächlichen Fachdomänen des
|
||||
österreichischen Turniersports.
|
||||
|
||||
Folgende Probleme wurden identifiziert:
|
||||
|
||||
1. Fehlende Ausrichtung zwischen Code-Struktur und ÖTO-Regelwerk
|
||||
2. Unklare Verantwortlichkeiten bei domänenübergreifenden Operationen (z.B. Nennungs-Transfer)
|
||||
3. Keine explizite Trennung zwischen Kern-Domäne (Nennungs-Workflow) und unterstützenden Domänen
|
||||
4. Fehlende Grundlage für eine skalierbare, offline-fähige Desktop-Architektur
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Das System wird in **6 Bounded Contexts** aufgeteilt, die als **Self-Contained Systems (SCS)** implementiert werden.
|
||||
Jeder Context ist fachlich eigenständig, besitzt seine eigene Ubiquitous Language und kommuniziert über definierte
|
||||
Schnittstellen.
|
||||
|
||||
### Übersicht der 6 Bounded Contexts
|
||||
|
||||
| Context | Verantwortlichkeit | Priorität | Phase |
|
||||
|----------------------------|------------------------------------------------------|-----------|---------|
|
||||
| `registration-context` | Nennungs-Workflow (Herzstück des Systems) | **P1** | Phase 4 |
|
||||
| `actor-context` | Reiter, Pferde, Funktionäre, Vereine, ZNS-Stammdaten | **P1** | Phase 4 |
|
||||
| `competition-context` | Bewerbe, Startlisten, Ergebnisse, Abteilungs-Logik | **P2** | Phase 5 |
|
||||
| `event-management-context` | Veranstaltung, Turnier, Ausschreibung, Genehmigungen | **P2** | Phase 5 |
|
||||
| `billing-context` | Abrechnung, Kassa, Gebühren, Konten | **P3** | Phase 6 |
|
||||
| `identity-context` | Authentifizierung, Rollen, Berechtigungen (Keycloak) | **P3** | Phase 6 |
|
||||
|
||||
> **Hinweis `series-context`:** Cups, Serien und Meisterschaften werden in Phase 2+ als eigenständiger Context
|
||||
> implementiert. Die Architektur ist von Anfang an dafür vorbereitet (pluggable Berechnungsmodell,
|
||||
> konfigurierbare Paar-Bindung). Kein Hard-Coding von Serien-Logik in anderen Contexts.
|
||||
|
||||
---
|
||||
|
||||
### Context-Beschreibungen
|
||||
|
||||
#### `registration-context` — Kern-Domäne (Core Domain)
|
||||
|
||||
**Verantwortlichkeit:** Der gesamte Lebenszyklus einer Nennung – von der Erstanmeldung bis zur Stornierung.
|
||||
|
||||
**Aggregate Roots:**
|
||||
|
||||
- `DomNennung` – Verbindliche Anmeldung eines Paares (Reiter & Pferd) zu einem Bewerb
|
||||
- `DomNennungsTransfer` – Transfer-Operation (kein Storno + Neu); Guthaben bleibt erhalten
|
||||
- `DomAbteilung` – Kleinste Einheit für Startlisten und Ergebnisse (mit Warn-Logik)
|
||||
|
||||
**Ubiquitous Language (Auswahl):**
|
||||
|
||||
- `Nennung`, `Nennschluss`, `Nachnenngebühr`, `Nennungs-Transfer`, `Override-Event`, `Startwunsch`
|
||||
|
||||
**Kern-Invarianten:**
|
||||
|
||||
- Eine Nennung ist immer einem Paar (Reiter + Pferd) zugeordnet
|
||||
- Nennungs-Transfer ist eine atomare Operation – kein Zwischenzustand ohne gültiges Paar
|
||||
- Regelwerk-Verstöße erzeugen **Warnungen** (niemals harte Fehler) + `Override-Event`
|
||||
|
||||
---
|
||||
|
||||
#### `actor-context` — Unterstützende Domäne (Supporting Domain)
|
||||
|
||||
**Verantwortlichkeit:** Stammdaten aller Akteure und Synchronisation mit dem ZNS (Zentrales Nennungs-System).
|
||||
|
||||
**Aggregate Roots:**
|
||||
|
||||
- `DomReiter` – Reiter mit Lizenz, Satznummer, Startkarte
|
||||
- `DomPferd` – Pferd mit Lebensnummer, Kopfnummer, Satznummer
|
||||
- `DomFunktionär` – Person mit Turnier-Rolle und Qualifikation
|
||||
- `DomVerein` – OEPS-Mitgliedsverein (Veranstalter)
|
||||
|
||||
**Ubiquitous Language (Auswahl):**
|
||||
|
||||
- `Satznummer`, `Lebensnummer`, `Kopfnummer`, `FEI-ID`, `Lizenz`, `Startkarte`, `Sperrliste`, `Gastreiter`
|
||||
|
||||
**Kern-Invarianten:**
|
||||
|
||||
- `Satznummer` ist der primäre Schlüssel für den ZNS-Datenaustausch
|
||||
- `Lebensnummer` und `Kopfnummer` sind **nicht** als Datenbankschlüssel geeignet (ZNS-Inkonsistenzen)
|
||||
- ZNS-Daten werden lokal gecacht (Offline-First); Synchronisation im Hintergrund
|
||||
|
||||
---
|
||||
|
||||
#### `competition-context` — Unterstützende Domäne (Supporting Domain)
|
||||
|
||||
**Verantwortlichkeit:** Strukturierung von Bewerben, Erstellung von Startlisten, Erfassung von Ergebnissen.
|
||||
|
||||
**Aggregate Roots:**
|
||||
|
||||
- `DomBewerb` – Einzelne sportliche Prüfung mit Bewerbsnummer, Sparte, Klasse, Richtverfahren
|
||||
- `DomAbteilung` – Untereinheit eines Bewerbs mit eigenem Teilnehmerkreis und Platzierung
|
||||
- `DomStartliste` – Geordnete Liste der Starter einer Abteilung
|
||||
- `DomErgebnis` – Ergebnis eines Starts (Platzierung, Punkte, Zeit)
|
||||
|
||||
**Ubiquitous Language (Auswahl):**
|
||||
|
||||
- `Bewerb`, `Prüfung`, `Abteilung`, `Abteilungsnummer`, `Startliste`, `Richtverfahren`, `Klasse/Höhe`
|
||||
|
||||
**Kern-Invarianten:**
|
||||
|
||||
- Abteilungs-Schwellenwerte gemäß ÖTO § 39 lösen **Warnungen** aus (→ `Override-Event`)
|
||||
- Vollständige Schwellenwert-Tabellen:
|
||||
`docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
|
||||
|
||||
---
|
||||
|
||||
#### `event-management-context` — Unterstützende Domäne (Supporting Domain)
|
||||
|
||||
**Verantwortlichkeit:** Verwaltung von Veranstaltungen und Turnieren, Ausschreibungs-Generierung, Genehmigungsprozesse.
|
||||
|
||||
**Aggregate Roots:**
|
||||
|
||||
- `DomVeranstaltung` – Interne Organisationseinheit des Veranstalters (selbst vergebene ID)
|
||||
- `DomTurnier` – Offizielles Turnier mit OEPS-vergebener Turniernummer
|
||||
- `DomAusschreibung` – Offizielles Dokument mit Pflichtfeldern gemäß ÖTO (A-Satz ZNS)
|
||||
|
||||
**Ubiquitous Language (Auswahl):**
|
||||
|
||||
- `Veranstaltung`, `Turnier`, `Turniernummer`, `Turnierkategorie`, `Ausschreibung`, `Kombination`, `TBA`
|
||||
|
||||
**Kern-Invarianten:**
|
||||
|
||||
- `Veranstaltung` ≠ `Turnier` (→ ADR-0002, ÖTO § 2 Abs. 1): Eine Veranstaltung kann mehrere Turniere umfassen
|
||||
- Turniernummern werden von der OEPS vergeben, nicht selbst generiert
|
||||
- Kombinations-Turniere behalten je eigene Turniernummer
|
||||
|
||||
---
|
||||
|
||||
#### `billing-context` — Generische Domäne (Generic Domain)
|
||||
|
||||
**Verantwortlichkeit:** Gebührenberechnung, Kassenführung, Abrechnung mit Reitern und dem Verband.
|
||||
|
||||
**Aggregate Roots:**
|
||||
|
||||
- `DomKonto` – Kontobasierte Abrechnung pro Zahler (Basis für „Hansi-Szenario")
|
||||
- `DomGebühr` – Einzelgebühr (Nenngeld, Nachnenngebühr, Sportförderbeitrag, Tierwohl-Euro)
|
||||
- `DomAbrechnung` – Zusammenfassung aller Gebühren einer Veranstaltung
|
||||
|
||||
**Ubiquitous Language (Auswahl):**
|
||||
|
||||
- `Konto`, `Nenngeld`, `Nachnenngebühr`, `Sportförderbeitrag`, `Tierwohl-Euro`, `Gebühren-Verzicht`
|
||||
|
||||
**Kern-Invarianten:**
|
||||
|
||||
- Sportförderbeitrag und Tierwohl-Euro fallen **pro Start** an (nicht pro Nennung)
|
||||
- Gebühren-Verzicht wird als explizites Event gespeichert (Audit-Trail)
|
||||
|
||||
---
|
||||
|
||||
#### `identity-context` — Generische Domäne (Generic Domain)
|
||||
|
||||
**Verantwortlichkeit:** Authentifizierung, Rollen-Management, Berechtigungsprüfung (via Keycloak).
|
||||
|
||||
**Aggregate Roots:**
|
||||
|
||||
- `DomBenutzer` – Systembenutzer mit Rollen (TBA, Veranstalter, Meldestelle, Richter)
|
||||
- `DomRolle` – Definierte Rolle mit Berechtigungen
|
||||
|
||||
**Ubiquitous Language (Auswahl):**
|
||||
|
||||
- `TBA`, `Veranstalter`, `Meldestelle`, `Richter`, `Rolle`, `Berechtigung`
|
||||
|
||||
**Kern-Invarianten:**
|
||||
|
||||
- Keycloak ist der einzige Identity Provider (→ ADR-0006)
|
||||
- Rollen sind turnierbezogen (ein Benutzer kann bei Turnier A TBA und bei Turnier B Richter sein)
|
||||
|
||||
---
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
### Positive
|
||||
|
||||
- **Fachliche Klarheit:** Jeder Context hat eine klar definierte Verantwortlichkeit und eigene Ubiquitous Language
|
||||
- **Unabhängige Entwicklung:** P1-Contexts (`registration-context`, `actor-context`) können ohne P2/P3 entwickelt werden
|
||||
- **Offline-First:** Jeder Context kann seinen eigenen lokalen Cache verwalten (SQLDelight)
|
||||
- **ÖTO-Konformität:** Die Context-Grenzen spiegeln die Struktur des ÖTO-Regelwerks wider
|
||||
- **Erweiterbarkeit:** `series-context` kann in Phase 2+ ohne Änderungen an bestehenden Contexts hinzugefügt werden
|
||||
|
||||
### Negative
|
||||
|
||||
- **Koordinationsaufwand:** Domänenübergreifende Use-Cases (z.B. Nennungs-Workflow) erfordern explizite Integration
|
||||
- **Datenkonsistenz:** Eventual Consistency zwischen Contexts muss bewusst gehandhabt werden
|
||||
- **Initialer Aufwand:** Vollständige Context-Implementierung erfordert mehr Vorabdesign
|
||||
|
||||
### Neutral
|
||||
|
||||
- Die Context-Grenzen können sich mit wachsendem Domänenwissen verschieben (Living Architecture)
|
||||
|
||||
## Betrachtete Alternativen
|
||||
|
||||
### Technische Modulaufteilung (abgelehnt)
|
||||
|
||||
Die ursprüngliche Aufteilung in `masterdata`, `members`, `horses`, `events` wurde verworfen, da sie technische
|
||||
Kategorien statt fachliche Domänen widerspiegelt und keine klare Heimat für den Nennungs-Workflow bietet.
|
||||
|
||||
### Monolithische Domäne (abgelehnt)
|
||||
|
||||
Ein einzelner großer Domänen-Context würde die Komplexität des ÖTO-Regelwerks nicht beherrschbar machen und
|
||||
die Offline-First-Strategie erschweren.
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [ADR-0001: Modulare Architektur](0001-modular-architecture-de.md)
|
||||
- [ADR-0002: Domain-Driven Design](0002-domain-driven-design-de.md)
|
||||
- [ADR-0015: Context Map](0015-context-map-de.md)
|
||||
- [Ubiquitous Language](../../03_Domain/01_Glossary/Ubiquitous_Language.md)
|
||||
- [Abteilungs-Trennungs-Schwellenwerte](../../03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md)
|
||||
- [MASTER_ROADMAP](../MASTER_ROADMAP.md)
|
||||
- ÖTO 2026, § 2 Abs. 1, § 2 Abs. 7, § 2 Abs. 8, § 39
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
type: ADR
|
||||
id: ADR-0015
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-24
|
||||
---
|
||||
|
||||
# ADR-0015: Context Map & Integration Patterns
|
||||
|
||||
## Status
|
||||
|
||||
Akzeptiert
|
||||
|
||||
## Kontext
|
||||
|
||||
Nach der Definition der 6 Bounded Contexts (→ ADR-0014) müssen die **Beziehungen zwischen den Contexts** explizit
|
||||
dokumentiert werden. Eine Context Map beschreibt:
|
||||
|
||||
- Welche Contexts miteinander kommunizieren
|
||||
- In welche Richtung Abhängigkeiten fließen
|
||||
- Welches Integration Pattern verwendet wird
|
||||
- Wo Anti-Corruption Layers (ACL) notwendig sind
|
||||
|
||||
Ohne eine explizite Context Map entstehen implizite Abhängigkeiten, die die Unabhängigkeit der Contexts untergraben
|
||||
und die Offline-First-Strategie gefährden.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
### Context Map (ASCII-Diagramm)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ EXTERNE SYSTEME │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ZNS (Zentrales Nennungs-System / OEPS) [Upstream / Big Ball] │ │
|
||||
│ └──────────────────────────┬──────────────────────────────────────────┘ │
|
||||
│ │ ACL (A-Satz / B-Satz Import) │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Keycloak (Identity Provider) [Upstream / Conformist] │ │
|
||||
│ └──────────────────────────┬───────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ JWT / OIDC
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ INTERNE CONTEXTS │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ identity-context │◄───────│ (alle Contexts) │ │
|
||||
│ │ [Generic Domain] │ OHS │ prüfen Berechtigungen via Token │ │
|
||||
│ └──────────────────────┘ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ACL ┌──────────────────────────────────────┐ │
|
||||
│ │ actor-context │◄───────│ ZNS (extern) │ │
|
||||
│ │ [Supporting Domain] │ └──────────────────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ DomReiter │ CL/SK ┌──────────────────────────────────────┐ │
|
||||
│ │ DomPferd │───────►│ registration-context │ │
|
||||
│ │ DomFunktionär │ │ [Core Domain] │ │
|
||||
│ │ DomVerein │ │ │ │
|
||||
│ └──────────────────────┘ │ DomNennung │ │
|
||||
│ │ DomNennungsTransfer │ │
|
||||
│ ┌──────────────────────┐ CL/SK │ DomAbteilung │ │
|
||||
│ │ event-management- │───────►│ │ │
|
||||
│ │ context │ └──────────────┬───────────────────────┘ │
|
||||
│ │ [Supporting Domain] │ │ │
|
||||
│ │ │ │ Domain Events │
|
||||
│ │ DomVeranstaltung │ │ (NennungErstellt, │
|
||||
│ │ DomTurnier │ │ NennungStorniert, │
|
||||
│ │ DomAusschreibung │ │ NennungTransferiert) │
|
||||
│ └──────────────────────┘ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ billing-context │◄───────│ competition-context │ │
|
||||
│ │ [Generic Domain] │ ACL │ [Supporting Domain] │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ DomKonto │ │ DomBewerb │ │
|
||||
│ │ DomGebühr │ │ DomAbteilung │ │
|
||||
│ │ DomAbrechnung │ │ DomStartliste │ │
|
||||
│ └──────────────────────┘ │ DomErgebnis │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ series-context [Phase 2+ — Architektur vorbereitet, nicht aktiv] │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Legende:**
|
||||
|
||||
- `ACL` = Anti-Corruption Layer
|
||||
- `CL/SK` = Customer/Supplier mit Shared Kernel (gemeinsame IDs)
|
||||
- `OHS` = Open Host Service (standardisiertes Interface)
|
||||
- `►` = Abhängigkeitsrichtung (Downstream → Upstream)
|
||||
|
||||
---
|
||||
|
||||
### Beziehungen im Detail
|
||||
|
||||
#### 1. ZNS (extern) → `actor-context`
|
||||
|
||||
**Pattern:** Upstream/Downstream mit **Anti-Corruption Layer (ACL)**
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Richtung | ZNS ist Upstream (Datenquelle), `actor-context` ist Downstream |
|
||||
| Protokoll | Datei-Import (A-Satz / B-Satz, proprietäres Format) |
|
||||
| ACL-Aufgabe | Übersetzung von ZNS-Datenformaten in interne Domänenmodelle |
|
||||
| Offline-Verhalten | ZNS-Daten werden lokal gecacht; Import läuft asynchron |
|
||||
| Kritische Regel | `Satznummer` ist primärer Schlüssel; `Lebensnummer` und `Kopfnummer` sind **nicht** als DB-Schlüssel geeignet (ZNS-Inkonsistenzen bekannt) |
|
||||
|
||||
**ACL-Verantwortlichkeiten:**
|
||||
|
||||
- Normalisierung inkonsistenter Felder (z.B. Farbe `"Braun"` vs. `"Brauner"`)
|
||||
- Generierung interner IDs für ausländische Pferde ohne UELN
|
||||
- Validierung und Ablehnung korrupter ZNS-Datensätze mit Protokollierung
|
||||
|
||||
---
|
||||
|
||||
#### 2. `actor-context` → `registration-context`
|
||||
|
||||
**Pattern:** Customer/Supplier mit **Shared Kernel (gemeinsame IDs)**
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|-------------------|--------------------------------------------------------------------------------|
|
||||
| Richtung | `actor-context` ist Upstream (Stammdaten-Lieferant) |
|
||||
| Shared Kernel | `ReiterId` (Satznummer), `PferdId` (Satznummer) als gemeinsame Referenz-IDs |
|
||||
| Kommunikation | Synchron: Lookup bei Nennungs-Erstellung; Asynchron: Sperrlisten-Updates |
|
||||
| Offline-Verhalten | `registration-context` hält lokale Kopie der benötigten Akteur-Daten |
|
||||
| Kritische Regel | `registration-context` darf Akteur-Daten **nicht** direkt mutieren (nur lesen) |
|
||||
|
||||
---
|
||||
|
||||
#### 3. `event-management-context` → `registration-context`
|
||||
|
||||
**Pattern:** Customer/Supplier mit **Shared Kernel (gemeinsame IDs)**
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|-----------------|---------------------------------------------------------------------|
|
||||
| Richtung | `event-management-context` ist Upstream (Turnier-/Bewerbs-Struktur) |
|
||||
| Shared Kernel | `TurnierId`, `BewerbId` als gemeinsame Referenz-IDs |
|
||||
| Kommunikation | Synchron: Bewerbs-Lookup bei Nennungs-Erstellung |
|
||||
| Kritische Regel | Nennungen können nur für existierende Bewerbe erstellt werden |
|
||||
|
||||
---
|
||||
|
||||
#### 4. `registration-context` → `competition-context`
|
||||
|
||||
**Pattern:** Upstream/Downstream via **Domain Events**
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|--------------------|-----------------------------------------------------------------------------|
|
||||
| Richtung | `registration-context` ist Upstream (Ereignis-Quelle) |
|
||||
| Events | `NennungErstelltEvent`, `NennungStorniertEvent`, `NennungTransferiertEvent` |
|
||||
| Kommunikation | Asynchron (Event Bus / lokale Event Queue) |
|
||||
| Aufgabe Downstream | `competition-context` baut Startlisten aus Nennungs-Events auf |
|
||||
| Offline-Verhalten | Events werden lokal persistiert und bei Verbindung synchronisiert |
|
||||
|
||||
---
|
||||
|
||||
#### 5. `registration-context` → `billing-context`
|
||||
|
||||
**Pattern:** Upstream/Downstream via **Domain Events** mit ACL
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|-----------------|-----------------------------------------------------------------------------------------------------------|
|
||||
| Richtung | `registration-context` ist Upstream (Gebühren-Auslöser) |
|
||||
| Events | `NennungErstelltEvent` (löst Nenngeld aus), `NennungStorniertEvent` (Gutschrift), `GebührenVerzichtEvent` |
|
||||
| ACL-Aufgabe | Übersetzung von Nennungs-Events in Gebühren-Buchungen |
|
||||
| Kritische Regel | Sportförderbeitrag und Tierwohl-Euro fallen **pro Start** an (nicht pro Nennung) |
|
||||
|
||||
---
|
||||
|
||||
#### 6. `competition-context` → `billing-context`
|
||||
|
||||
**Pattern:** Upstream/Downstream via **Domain Events** mit ACL
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|-------------|------------------------------------------------------------------|
|
||||
| Richtung | `competition-context` ist Upstream |
|
||||
| Events | `StartErfolgreich` (löst Sportförderbeitrag + Tierwohl-Euro aus) |
|
||||
| ACL-Aufgabe | Übersetzung von Start-Events in Gebühren-Buchungen |
|
||||
|
||||
---
|
||||
|
||||
#### 7. Keycloak → alle Contexts
|
||||
|
||||
**Pattern:** Upstream/Downstream, **Conformist** (alle Contexts passen sich Keycloak an)
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|-------------------|-----------------------------------------------------------------------------------|
|
||||
| Richtung | Keycloak ist Upstream (Identity Provider) |
|
||||
| Protokoll | OIDC / JWT-Token |
|
||||
| Kommunikation | Synchron: Token-Validierung bei jedem Request |
|
||||
| Offline-Verhalten | Token-Caching mit konfigurierbarer TTL; Offline-Modus mit eingeschränkten Rechten |
|
||||
|
||||
---
|
||||
|
||||
### Anti-Corruption Layer (ACL) — Implementierungsrichtlinien
|
||||
|
||||
Jeder ACL wird als **eigenständiges Modul** innerhalb des Downstream-Contexts implementiert:
|
||||
|
||||
```
|
||||
actor-context/
|
||||
└── infrastructure/
|
||||
└── zns/
|
||||
├── ZnsImportService.kt # Orchestrierung
|
||||
├── ZnsAkteurMapper.kt # Übersetzung ZNS → Dom*
|
||||
├── ZnsValidationFilter.kt # Ablehnung korrupter Daten
|
||||
└── ZnsImportProtokoll.kt # Audit-Log aller Imports
|
||||
```
|
||||
|
||||
**Prinzipien:**
|
||||
|
||||
1. Der ACL übersetzt **immer** in die interne Ubiquitous Language — niemals umgekehrt
|
||||
2. Fehlerhafte externe Daten werden **protokolliert und übersprungen** (kein Systemabsturz)
|
||||
3. Der ACL ist der einzige Ort, der das externe Datenformat kennt
|
||||
|
||||
---
|
||||
|
||||
### Offline-First Integration
|
||||
|
||||
Da die Anwendung als Desktop-App (Offline-First) betrieben wird, gelten folgende Regeln:
|
||||
|
||||
| Szenario | Verhalten |
|
||||
|-----------------------------|------------------------------------------------------------|
|
||||
| ZNS nicht erreichbar | Lokaler Cache wird verwendet; Import-Status wird angezeigt |
|
||||
| Nennungs-Erstellung offline | Lokal gespeichert; Events werden bei Sync übertragen |
|
||||
| Keycloak nicht erreichbar | Gecachter Token wird verwendet (TTL-basiert) |
|
||||
| Konflikt bei Sync | Optimistic Locking (409) + manuelle Auflösung (→ ADR-0013) |
|
||||
|
||||
---
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
### Positive
|
||||
|
||||
- **Explizite Abhängigkeiten:** Alle Context-Beziehungen sind dokumentiert und nachvollziehbar
|
||||
- **Schutz der Kern-Domäne:** `registration-context` ist durch ACLs von externen Systemen isoliert
|
||||
- **Offline-Fähigkeit:** Jede Integration ist auf Offline-Betrieb ausgelegt
|
||||
- **Erweiterbarkeit:** `series-context` kann in Phase 2+ als reiner Downstream-Consumer hinzugefügt werden
|
||||
|
||||
### Negative
|
||||
|
||||
- **Komplexität:** ACLs und Event-Übersetzungen erhöhen den initialen Implementierungsaufwand
|
||||
- **Eventual Consistency:** Zwischen `registration-context` und `competition-context` gibt es keine sofortige Konsistenz
|
||||
|
||||
### Neutral
|
||||
|
||||
- Die Context Map ist ein **lebendes Dokument** und wird mit jeder neuen Integration aktualisiert
|
||||
|
||||
## Betrachtete Alternativen
|
||||
|
||||
### Direkte Context-zu-Context-Aufrufe (abgelehnt)
|
||||
|
||||
Direkte synchrone Aufrufe zwischen Contexts würden enge Kopplung erzeugen und die Offline-Fähigkeit untergraben.
|
||||
|
||||
### Shared Database (abgelehnt)
|
||||
|
||||
Eine gemeinsame Datenbank für alle Contexts würde die Context-Grenzen aufweichen und die unabhängige Entwicklung
|
||||
verhindern.
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [ADR-0014: Bounded Context Mapping](0014-bounded-context-mapping-de.md)
|
||||
- [ADR-0004: Event-Driven Communication](0004-event-driven-communication-de.md)
|
||||
- [ADR-0006: Authentication & Authorization (Keycloak)](0006-authentication-authorization-keycloak-de.md)
|
||||
- [ADR-0013: Tech Stack Stabilization](0013-tech-stack-stabilization-2026.md)
|
||||
- [Ubiquitous Language](../../03_Domain/01_Glossary/Ubiquitous_Language.md)
|
||||
- [MASTER_ROADMAP](../MASTER_ROADMAP.md)
|
||||
- Vaughn Vernon: „Implementing Domain-Driven Design", Kapitel 3 (Context Maps)
|
||||
- ÖTO 2026, ZNS-Schnittstellen-Spezifikation (A-Satz / B-Satz)
|
||||
@@ -0,0 +1,561 @@
|
||||
---
|
||||
type: ADR
|
||||
id: ADR-0016
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-24
|
||||
---
|
||||
|
||||
# ADR-0016: API-Design & Anti-Corruption Layer (ACL)
|
||||
|
||||
## Status
|
||||
|
||||
Akzeptiert
|
||||
|
||||
## Kontext
|
||||
|
||||
Die 6 Bounded Contexts (ADR-0014) kommunizieren über definierte Schnittstellen (ADR-0015).
|
||||
Dieses ADR konkretisiert:
|
||||
|
||||
1. **Welche Daten** über Kontextgrenzen fließen (DTOs / Query-Objekte)
|
||||
2. **Wie** die ACL-Schicht technisch implementiert wird (Ports & Adapters)
|
||||
3. **Welche REST-Endpunkte** die P1-Contexts nach außen exponieren
|
||||
4. **Welche Domain Events** asynchron publiziert werden
|
||||
|
||||
Grundprinzip: **Kein Context kennt die internen Modelle eines anderen Context.**
|
||||
Jeder Context übersetzt eingehende Daten in seine eigene Ubiquitous Language.
|
||||
|
||||
---
|
||||
|
||||
## Entscheidung
|
||||
|
||||
### 1. Architektur-Muster: Ports & Adapters (Hexagonal)
|
||||
|
||||
Jeder Context implementiert:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ [Context X] │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────────┐ ┌───────────┐ │
|
||||
│ │ Inbound │ │ Domain │ │ Outbound │ │
|
||||
│ │ Port │───▶│ Model │───▶│ Port │ │
|
||||
│ │(REST/UI) │ │ (Aggregate) │ │(ACL/Event)│ │
|
||||
│ └──────────┘ └──────────────┘ └───────────┘ │
|
||||
│ ▲ │ │
|
||||
│ │DTO DTO │ │
|
||||
└───────┼────────────────────────────────────┼─────────┘
|
||||
│ │
|
||||
[Client / [Anderer
|
||||
Frontend] Context]
|
||||
```
|
||||
|
||||
**Regel:** DTOs sind flache, serialisierbare Datenstrukturen ohne Domänen-Logik.
|
||||
Domain-Objekte verlassen den Context **niemals**.
|
||||
|
||||
---
|
||||
|
||||
### 2. Schnittstellen-Katalog: P1-Contexts
|
||||
|
||||
#### 2.1 `actor-context` → Inbound REST API
|
||||
|
||||
**Base-URL:** `/api/v1/actors`
|
||||
|
||||
| Methode | Pfad | Beschreibung | Response-DTO |
|
||||
|---------|--------------------------------|------------------------------------|--------------------------|
|
||||
| GET | `/reiter/{satznummer}` | Reiter per Satznummer laden | `ReiterDto` |
|
||||
| GET | `/reiter/search?name=&verein=` | Reiter suchen (für Nennungs-Maske) | `List<ReiterSummaryDto>` |
|
||||
| GET | `/pferde/{lebensnummer}` | Pferd per Lebensnummer laden | `PferdDto` |
|
||||
| GET | `/pferde/search?name=&reiter=` | Pferde suchen (für Nennungs-Maske) | `List<PferdSummaryDto>` |
|
||||
| GET | `/funktionaere/{id}` | Funktionär laden | `FunktionaerDto` |
|
||||
| GET | `/vereine/{vereinsnummer}` | Verein laden | `VereinDto` |
|
||||
| POST | `/reiter` | Reiter anlegen (TBA-Workflow) | `ReiterDto` |
|
||||
| PUT | `/reiter/{satznummer}` | Reiter aktualisieren | `ReiterDto` |
|
||||
|
||||
**DTOs (actor-context → outbound):**
|
||||
|
||||
```kotlin
|
||||
// Vollständiges Reiter-Objekt (für Detail-Ansicht)
|
||||
data class ReiterDto(
|
||||
val satznummer: String, // OEPS-Satznummer (eindeutig)
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val geburtsdatum: LocalDate,
|
||||
val lizenzklasse: String, // "A", "B", "C", "AMATEUR" etc.
|
||||
val vereinsnummer: String,
|
||||
val vereinsname: String,
|
||||
val startkarte: Boolean,
|
||||
val znsId: String? // ZNS-Referenz (nullable, Offline-Fall)
|
||||
)
|
||||
|
||||
// Kompaktes Objekt für Suchergebnisse / Dropdown
|
||||
data class ReiterSummaryDto(
|
||||
val satznummer: String,
|
||||
val vollname: String, // "Nachname, Vorname"
|
||||
val lizenzklasse: String,
|
||||
val vereinsname: String
|
||||
)
|
||||
|
||||
// Vollständiges Pferd-Objekt
|
||||
data class PferdDto(
|
||||
val lebensnummer: String, // FEI-Lebensnummer (eindeutig)
|
||||
val name: String,
|
||||
val kopfnummer: String?, // Turnier-Kopfnummer (optional)
|
||||
val satznummer: String?, // OEPS-Satznummer des Besitzers
|
||||
val rasse: String?,
|
||||
val farbe: String?,
|
||||
val geburtsjahr: Int?
|
||||
)
|
||||
|
||||
data class PferdSummaryDto(
|
||||
val lebensnummer: String,
|
||||
val name: String,
|
||||
val kopfnummer: String?
|
||||
)
|
||||
|
||||
data class FunktionaerDto(
|
||||
val id: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val rolle: String, // "RICHTER", "PARCOURSCHEF" etc.
|
||||
val qualifikationen: List<String>
|
||||
)
|
||||
|
||||
data class VereinDto(
|
||||
val vereinsnummer: String,
|
||||
val name: String,
|
||||
val oepsNummer: String
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.2 `event-management-context` → Inbound REST API
|
||||
|
||||
**Base-URL:** `/api/v1/events`
|
||||
|
||||
| Methode | Pfad | Beschreibung | Response-DTO |
|
||||
|---------|-------------------------------------|-------------------------------------|---------------------------------|
|
||||
| GET | `/veranstaltungen` | Alle Veranstaltungen (paginiert) | `Page<VeranstaltungSummaryDto>` |
|
||||
| GET | `/veranstaltungen/{id}` | Veranstaltung mit Turnieren laden | `VeranstaltungDetailDto` |
|
||||
| GET | `/turniere/{turniernummer}` | Turnier per OEPS-Nummer laden | `TurnierDto` |
|
||||
| GET | `/turniere/{turniernummer}/bewerbe` | Bewerbe eines Turniers | `List<BewerbSummaryDto>` |
|
||||
| POST | `/veranstaltungen` | Neue Veranstaltung anlegen | `VeranstaltungDto` |
|
||||
| POST | `/veranstaltungen/{id}/turniere` | Turnier zu Veranstaltung hinzufügen | `TurnierDto` |
|
||||
|
||||
**DTOs (event-management-context → outbound):**
|
||||
|
||||
```kotlin
|
||||
data class VeranstaltungSummaryDto(
|
||||
val id: String,
|
||||
val bezeichnung: String,
|
||||
val datum: LocalDate,
|
||||
val ort: String,
|
||||
val veranstalterVereinsnummer: String,
|
||||
val status: String // "GEPLANT", "AKTIV", "ABGESCHLOSSEN"
|
||||
)
|
||||
|
||||
data class VeranstaltungDetailDto(
|
||||
val id: String,
|
||||
val bezeichnung: String,
|
||||
val datum: LocalDate,
|
||||
val ort: String,
|
||||
val veranstalterVereinsnummer: String,
|
||||
val status: String,
|
||||
val turniere: List<TurnierSummaryDto>
|
||||
)
|
||||
|
||||
data class TurnierDto(
|
||||
val turniernummer: String, // OEPS-vergebene Nummer
|
||||
val veranstaltungId: String, // Referenz auf interne Veranstaltung
|
||||
val bezeichnung: String,
|
||||
val datum: LocalDate,
|
||||
val kategorie: String, // "LT", "RT", "BT", "ST" etc.
|
||||
val sparten: List<String> // ["DRESSUR", "SPRINGEN"] etc.
|
||||
)
|
||||
|
||||
data class TurnierSummaryDto(
|
||||
val turniernummer: String,
|
||||
val bezeichnung: String,
|
||||
val datum: LocalDate,
|
||||
val kategorie: String
|
||||
)
|
||||
|
||||
// Wird vom registration-context konsumiert (ACL-Übersetzung)
|
||||
data class BewerbSummaryDto(
|
||||
val bewerbId: String,
|
||||
val bewerbsnummer: String, // z.B. "1", "2A", "2B"
|
||||
val bezeichnung: String,
|
||||
val sparte: String,
|
||||
val klasse: String,
|
||||
val maxStarter: Int?
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.3 `registration-context` → Inbound REST API
|
||||
|
||||
**Base-URL:** `/api/v1/registrations`
|
||||
|
||||
| Methode | Pfad | Beschreibung | Response-DTO |
|
||||
|---------|-------------------------------------------------|-------------------------------------|---------------------------|
|
||||
| GET | `/nennungen?turniernummer=&status=` | Nennungen filtern | `List<NennungSummaryDto>` |
|
||||
| GET | `/nennungen/{nennungsId}` | Einzelne Nennung laden | `NennungDetailDto` |
|
||||
| POST | `/nennungen` | Neue Nennung einreichen | `NennungDetailDto` |
|
||||
| PUT | `/nennungen/{nennungsId}/status` | Status ändern (Storno, Bestätigung) | `NennungDetailDto` |
|
||||
| POST | `/nennungen/{nennungsId}/transfer` | Nennung transferieren | `NennungsTransferDto` |
|
||||
| GET | `/nennungen/{nennungsId}/transfer/{transferId}` | Transfer-Status abfragen | `NennungsTransferDto` |
|
||||
|
||||
**DTOs (registration-context):**
|
||||
|
||||
```kotlin
|
||||
// Eingehend: Neue Nennung (Command)
|
||||
data class NennungErstellenCommand(
|
||||
val turniernummer: String,
|
||||
val bewerbId: String,
|
||||
val reiterSatznummer: String,
|
||||
val pferdLebensnummer: String,
|
||||
val startwunsch: String?, // "FRUEH", "SPAET", "EGAL"
|
||||
val bemerkung: String?
|
||||
)
|
||||
|
||||
// Ausgehend: Kompakte Nennung (für Listen)
|
||||
data class NennungSummaryDto(
|
||||
val nennungsId: String,
|
||||
val turniernummer: String,
|
||||
val bewerbBezeichnung: String,
|
||||
val reiterName: String, // Denormalisiert für Performance
|
||||
val pferdName: String, // Denormalisiert für Performance
|
||||
val status: String, // "EINGEREICHT", "BESTAETIGT", "STORNIERT"
|
||||
val eingereichtAm: LocalDateTime
|
||||
)
|
||||
|
||||
// Ausgehend: Vollständige Nennung (für Detail-Ansicht)
|
||||
data class NennungDetailDto(
|
||||
val nennungsId: String,
|
||||
val turniernummer: String,
|
||||
val bewerbId: String,
|
||||
val bewerbBezeichnung: String,
|
||||
val reiterSatznummer: String,
|
||||
val reiterName: String,
|
||||
val pferdLebensnummer: String,
|
||||
val pferdName: String,
|
||||
val startwunsch: String?,
|
||||
val status: String,
|
||||
val bemerkung: String?,
|
||||
val eingereichtAm: LocalDateTime,
|
||||
val letzteAenderung: LocalDateTime
|
||||
)
|
||||
|
||||
// Transfer-DTO
|
||||
data class NennungsTransferDto(
|
||||
val transferId: String,
|
||||
val quellNennungsId: String,
|
||||
val zielBewerbId: String,
|
||||
val zielReiterSatznummer: String?, // null = gleicher Reiter
|
||||
val zielPferdLebensnummer: String?, // null = gleiches Pferd
|
||||
val status: String, // "BEANTRAGT", "GENEHMIGT", "ABGELEHNT"
|
||||
val guthabenErhalten: Boolean
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ACL-Implementierung: registration-context konsumiert actor-context
|
||||
|
||||
Der `registration-context` benötigt Reiter- und Pferd-Daten, darf aber **nicht** direkt auf
|
||||
die Datenbank des `actor-context` zugreifen.
|
||||
|
||||
**ACL-Port (Interface im registration-context):**
|
||||
|
||||
```kotlin
|
||||
// Port-Interface (Teil des registration-context Domain-Layers)
|
||||
interface AktorReferenzPort {
|
||||
fun ladeReiter(satznummer: String): ReiterReferenz?
|
||||
fun ladePferd(lebensnummer: String): PferdReferenz?
|
||||
fun validiereReiterPferdKombination(
|
||||
reiterSatznummer: String,
|
||||
pferdLebensnummer: String
|
||||
): ValidiertesReiterPferdPaar?
|
||||
}
|
||||
|
||||
// Interne Referenz-Objekte (registration-context eigene Sprache)
|
||||
data class ReiterReferenz(
|
||||
val satznummer: String,
|
||||
val vollname: String,
|
||||
val lizenzklasse: LizenzKlasseE, // Eigener Enum des registration-context
|
||||
val vereinsnummer: String,
|
||||
val istStartberechtigt: Boolean // Abgeleitetes Feld (Startkarte + Lizenz aktiv)
|
||||
)
|
||||
|
||||
data class PferdReferenz(
|
||||
val lebensnummer: String,
|
||||
val name: String,
|
||||
val kopfnummer: String?
|
||||
)
|
||||
|
||||
data class ValidiertesReiterPferdPaar(
|
||||
val reiter: ReiterReferenz,
|
||||
val pferd: PferdReferenz,
|
||||
val paarungGueltig: Boolean,
|
||||
val warnungen: List<String> // z.B. "Pferd hat keine aktive Kopfnummer"
|
||||
)
|
||||
```
|
||||
|
||||
**ACL-Adapter (Infrastruktur-Layer, implementiert den Port):**
|
||||
|
||||
```kotlin
|
||||
// Adapter übersetzt actor-context DTO → registration-context Referenz-Objekt
|
||||
@Component
|
||||
class AktorReferenzAdapter(
|
||||
private val aktorClient: AktorContextClient // HTTP-Client oder direkte Bean
|
||||
) : AktorReferenzPort {
|
||||
|
||||
override fun ladeReiter(satznummer: String): ReiterReferenz? {
|
||||
val dto = aktorClient.getReiter(satznummer) ?: return null
|
||||
// ACL-Übersetzung: ReiterDto → ReiterReferenz
|
||||
return ReiterReferenz(
|
||||
satznummer = dto.satznummer,
|
||||
vollname = "${dto.nachname}, ${dto.vorname}",
|
||||
lizenzklasse = LizenzKlasseE.valueOf(dto.lizenzklasse),
|
||||
vereinsnummer = dto.vereinsnummer,
|
||||
istStartberechtigt = dto.startkarte
|
||||
)
|
||||
}
|
||||
|
||||
override fun ladePferd(lebensnummer: String): PferdReferenz? {
|
||||
val dto = aktorClient.getPferd(lebensnummer) ?: return null
|
||||
return PferdReferenz(
|
||||
lebensnummer = dto.lebensnummer,
|
||||
name = dto.name,
|
||||
kopfnummer = dto.kopfnummer
|
||||
)
|
||||
}
|
||||
|
||||
override fun validiereReiterPferdKombination(
|
||||
reiterSatznummer: String,
|
||||
pferdLebensnummer: String
|
||||
): ValidiertesReiterPferdPaar? {
|
||||
val reiter = ladeReiter(reiterSatznummer) ?: return null
|
||||
val pferd = ladePferd(pferdLebensnummer) ?: return null
|
||||
val warnungen = mutableListOf<String>()
|
||||
if (pferd.kopfnummer == null) warnungen.add("Pferd hat keine aktive Kopfnummer")
|
||||
if (!reiter.istStartberechtigt) warnungen.add("Reiter hat keine gültige Startkarte")
|
||||
return ValidiertesReiterPferdPaar(reiter, pferd, warnungen.isEmpty(), warnungen)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. ACL-Implementierung: registration-context konsumiert event-management-context
|
||||
|
||||
```kotlin
|
||||
// Port-Interface
|
||||
interface TurnierReferenzPort {
|
||||
fun ladeTurnier(turniernummer: String): TurnierReferenz?
|
||||
fun ladeBewerb(bewerbId: String): BewerbReferenz?
|
||||
fun ladeBewerbeDesTurniers(turniernummer: String): List<BewerbReferenz>
|
||||
}
|
||||
|
||||
// Interne Referenz-Objekte (registration-context Sprache)
|
||||
data class TurnierReferenz(
|
||||
val turniernummer: String,
|
||||
val bezeichnung: String,
|
||||
val datum: LocalDate,
|
||||
val kategorie: TurnierkategorieE, // Eigener Enum
|
||||
val istNennungMoeglich: Boolean // Abgeleitetes Feld (Datum + Status)
|
||||
)
|
||||
|
||||
data class BewerbReferenz(
|
||||
val bewerbId: String,
|
||||
val bewerbsnummer: String,
|
||||
val bezeichnung: String,
|
||||
val sparte: SparteE, // Eigener Enum
|
||||
val klasse: String,
|
||||
val maxStarter: Int?
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Domain Events (Asynchrone Kommunikation)
|
||||
|
||||
Folgende Events werden über den internen Event-Bus publiziert:
|
||||
|
||||
#### 5.1 `actor-context` publiziert
|
||||
|
||||
| Event | Payload (Schlüsselfelder) | Konsumenten |
|
||||
|----------------------|--------------------------------------------|----------------------------------------------|
|
||||
| `ReiterAktualisiert` | `satznummer`, `lizenzklasse`, `startkarte` | `registration-context` (Cache-Invalidierung) |
|
||||
| `PferdAktualisiert` | `lebensnummer`, `kopfnummer` | `registration-context` (Cache-Invalidierung) |
|
||||
| `ReiterGesperrt` | `satznummer`, `grund` | `registration-context` (Warn-Logik) |
|
||||
|
||||
#### 5.2 `registration-context` publiziert
|
||||
|
||||
| Event | Payload (Schlüsselfelder) | Konsumenten |
|
||||
|-----------------------|------------------------------------------------------------------------------------|------------------------------------------|
|
||||
| `NennungEingereicht` | `nennungsId`, `turniernummer`, `bewerbId`, `reiterSatznummer`, `pferdLebensnummer` | `billing-context`, `competition-context` |
|
||||
| `NennungStorniert` | `nennungsId`, `turniernummer`, `grund` | `billing-context` |
|
||||
| `NennungTransferiert` | `transferId`, `quellNennungsId`, `zielBewerbId` | `billing-context` |
|
||||
|
||||
#### 5.3 `event-management-context` publiziert
|
||||
|
||||
| Event | Payload (Schlüsselfelder) | Konsumenten |
|
||||
|---------------------------|-------------------------------------|--------------------------------------------|
|
||||
| `TurnierEroeffnet` | `turniernummer`, `datum`, `sparten` | `registration-context` (Nennungs-Freigabe) |
|
||||
| `NennungsschlussErreicht` | `turniernummer`, `zeitpunkt` | `registration-context` (Sperr-Logik) |
|
||||
| `TurnierAbgesagt` | `turniernummer`, `grund` | `registration-context`, `billing-context` |
|
||||
|
||||
**Event-Struktur (Basis):**
|
||||
|
||||
```kotlin
|
||||
// Basis-Event (alle Domain Events erben davon)
|
||||
abstract class DomainEvent(
|
||||
val eventId: String = UUID.randomUUID().toString(),
|
||||
val occurredAt: Instant = Instant.now(),
|
||||
val contextSource: String // z.B. "actor-context"
|
||||
)
|
||||
|
||||
// Beispiel: NennungEingereicht
|
||||
data class NennungEingereichtEvent(
|
||||
val nennungsId: String,
|
||||
val turniernummer: String,
|
||||
val bewerbId: String,
|
||||
val reiterSatznummer: String,
|
||||
val pferdLebensnummer: String,
|
||||
val eingereichtVon: String // Benutzer-ID (identity-context Referenz)
|
||||
) : DomainEvent(contextSource = "registration-context")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Offline-First: Lokale Referenz-Caches
|
||||
|
||||
Da die Desktop-App offline-fähig sein muss, cachen die ACL-Adapter Referenz-Daten lokal:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ registration-context (Desktop) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ AktorReferenzAdapter │ │
|
||||
│ │ │ │
|
||||
│ │ ladeReiter() ──▶ [Lokaler Cache (SQLite)] │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ Cache-Miss │ │
|
||||
│ │ [HTTP → actor-context] │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ Offline │ │
|
||||
│ │ [Fehler: ReiterNichtGefunden] │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Cache-Strategie:**
|
||||
|
||||
- **Reiter/Pferd-Daten:** Werden beim Turnier-Download vollständig gecacht (Bulk-Sync)
|
||||
- **Cache-Invalidierung:** Via `ReiterAktualisiert`-Event (wenn online) oder manueller Sync
|
||||
- **Offline-Fallback:** Gecachte Daten sind gültig; neue Reiter können nicht angelegt werden
|
||||
|
||||
---
|
||||
|
||||
### 7. ZNS-Schnittstelle (Externer Context)
|
||||
|
||||
Der `actor-context` implementiert eine ACL zum externen ZNS (Zentrales Nennungs-System):
|
||||
|
||||
```kotlin
|
||||
// Port (actor-context → ZNS)
|
||||
interface ZnsPort {
|
||||
fun ladeReiterAusSatz(satznummer: String): ZnsReiterSatz?
|
||||
fun ladePferdAusSatz(lebensnummer: String): ZnsPferdSatz?
|
||||
fun synchronisiereStammdaten(turniernummer: String): ZnsSyncErgebnis
|
||||
}
|
||||
|
||||
// ZNS A-Satz (Reiter-Stammdaten)
|
||||
data class ZnsReiterSatz(
|
||||
val satznummer: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val geburtsdatum: LocalDate,
|
||||
val lizenzklasse: String, // ZNS-Kodierung → wird in LizenzKlasseE übersetzt
|
||||
val vereinsNummer: String,
|
||||
val startkarte: Boolean
|
||||
)
|
||||
|
||||
// ZNS B-Satz (Pferd-Stammdaten)
|
||||
data class ZnsPferdSatz(
|
||||
val lebensnummer: String,
|
||||
val name: String,
|
||||
val satznummer: String?, // Besitzer-Satznummer
|
||||
val rasse: String?,
|
||||
val farbe: String?
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
### Positiv
|
||||
|
||||
- **Klare Kontraktgrenzen:** Jeder Context hat explizite, versionierbare APIs
|
||||
- **Unabhängige Deployments:** Contexts können unabhängig deployed werden
|
||||
- **Testbarkeit:** ACL-Ports können einfach gemockt werden (Unit-Tests)
|
||||
- **Offline-Fähigkeit:** Lokale Caches ermöglichen Offline-Betrieb ohne Architektur-Bruch
|
||||
- **ZNS-Isolation:** Änderungen am ZNS-Format betreffen nur den `ZnsAdapter`
|
||||
|
||||
### Negativ / Risiken
|
||||
|
||||
- **Datenduplizierung:** Denormalisierte Felder in DTOs (z.B. `reiterName` in `NennungSummaryDto`)
|
||||
- **Cache-Konsistenz:** Lokale Caches können veralten (akzeptiertes Risiko, Warn-Logik)
|
||||
- **Initialer Aufwand:** ACL-Adapter müssen für jeden Context implementiert werden
|
||||
|
||||
### Neutral
|
||||
|
||||
- **DTO-Versionierung:** Bei Breaking Changes muss API-Version erhöht werden (`/api/v2/...`)
|
||||
- **Event-Ordering:** Domain Events sind best-effort; kritische Operationen bleiben synchron
|
||||
|
||||
---
|
||||
|
||||
## Abgelehnte Alternativen
|
||||
|
||||
### Shared Domain Model
|
||||
|
||||
Alle Contexts teilen ein gemeinsames Domain-Modell (z.B. `DomReiter` überall).
|
||||
**Abgelehnt:** Führt zu starker Kopplung; Änderungen an `DomReiter` brechen alle Contexts.
|
||||
|
||||
### GraphQL Federation
|
||||
|
||||
Einheitliches GraphQL-Schema über alle Contexts.
|
||||
**Abgelehnt:** Zu komplex für MVP; REST + Domain Events reicht für die aktuelle Skalierung.
|
||||
|
||||
### Direkte Datenbank-Joins
|
||||
|
||||
`registration-context` liest direkt aus der `actor-context`-Datenbank.
|
||||
**Abgelehnt:** Verletzt SCS-Prinzip; verhindert unabhängige Deployments und Skalierung.
|
||||
|
||||
---
|
||||
|
||||
## Implementierungs-Reihenfolge (P1-Priorität)
|
||||
|
||||
1. **`actor-context` REST API** (`/api/v1/actors`) – Basis für alle anderen Contexts
|
||||
2. **`event-management-context` REST API** (`/api/v1/events`) – Turnier/Bewerb-Referenzen
|
||||
3. **ACL-Adapter im `registration-context`** – `AktorReferenzAdapter` + `TurnierReferenzAdapter`
|
||||
4. **`registration-context` REST API** (`/api/v1/registrations`) – Kern-Use-Cases
|
||||
5. **Domain Events** – `NennungEingereicht` als erstes Event (für `billing-context`)
|
||||
6. **Offline-Cache** – Bulk-Sync beim Turnier-Download
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [ADR-0014: Bounded Context Mapping](0014-bounded-context-mapping-de.md)
|
||||
- [ADR-0015: Context Map & Integration Patterns](0015-context-map-de.md)
|
||||
- [ADR-0004: Event-Driven Communication](0004-event-driven-communication-de.md)
|
||||
- [ADR-0001: Modular Architecture](0001-modular-architecture-de.md)
|
||||
- [Ubiquitous Language](../../03_Domain/01_Glossary/Ubiquitous_Language.md)
|
||||
- [MASTER_ROADMAP](../MASTER_ROADMAP.md)
|
||||
- Vaughn Vernon: „Implementing Domain-Driven Design", Kapitel 13 (Integrating Bounded Contexts)
|
||||
- ÖTO 2026, ZNS-Schnittstellen-Spezifikation (A-Satz / B-Satz)
|
||||
@@ -14,5 +14,8 @@ Namensschema: ADR-XXX-title.md mit fortlaufender Nummerierung.
|
||||
- ADR-003 Optimistic Locking (409) als Konfliktstrategie
|
||||
- ADR-004 Freshness UI (Ampel)
|
||||
- ADR-005 Core Domain & Feature Isolation
|
||||
- ADR-0014 Bounded Context Mapping & Aggregate Roots
|
||||
- ADR-0015 Context Map & Integration Patterns
|
||||
- ADR-0016 API-Design & Anti-Corruption Layer (ACL)
|
||||
|
||||
Siehe Template: ADR-000-template.md.
|
||||
|
||||
Reference in New Issue
Block a user