- 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>
562 lines
22 KiB
Markdown
562 lines
22 KiB
Markdown
---
|
||
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)
|