refactoring Single Source of Truth

This commit is contained in:
2025-09-13 22:04:20 +02:00
parent caaa4114ee
commit 8eb7e6f773
26 changed files with 5544 additions and 169 deletions
@@ -0,0 +1,508 @@
# Architecture Principles und Grundsätze
---
guideline_type: "project-standards"
scope: "architecture-principles"
audience: ["developers", "architects", "ai-assistants"]
last_updated: "2025-09-13"
dependencies: ["master-guideline.md"]
related_files: ["build.gradle.kts", "settings.gradle.kts", "docker-compose.yml"]
ai_context: "Architectural foundations, microservices patterns, DDD principles, event-driven architecture, and multiplatform strategy"
---
## 🏗️ Vision & Architektonische Grundpfeiler
Dieses Dokument definiert die verbindlichen technischen Richtlinien und Qualitätsstandards für das Projekt "Meldestelle_Pro". Ziel ist die Schaffung einer modernen, skalierbaren und wartbaren Plattform für den Pferdesport.
> **🤖 AI-Assistant Hinweis:**
> Die Architektur basiert auf vier Kernsäulen:
> - **Microservices:** Modularität & Skalierbarkeit
> - **DDD:** Fachlichkeit im Code
> - **EDA:** Ereignisgesteuerte Entkopplung
> - **KMP:** Kotlin Multiplatform für Effizienz
### Die vier Säulen der Architektur
1. **Modularität & Skalierbarkeit** durch eine **Microservices-Architektur**
2. **Fachlichkeit im Code** durch **Domain-Driven Design (DDD)**
3. **Entkopplung & Resilienz** durch eine **ereignisgesteuerte Architektur (EDA)**
4. **Effizienz & Konsistenz** durch eine **Multiplattform-Client-Strategie (KMP)**
> **Grundsatz:** Jede Code-Änderung muss diese vier Grundprinzipien respektieren.
## 🎯 AI-Assistenten: Architektur-Schnellreferenz
### Architektur-Säulen im Detail
| Säule | Technologie | Zweck | Umsetzung |
|-------|------------|-------|-----------|
| Microservices | Spring Boot, Docker | Modularität & Skalierbarkeit | Service-per-Domain-Pattern |
| DDD | Kotlin, Clean Architecture | Fachlichkeit im Code | Bounded Contexts, Domain Events |
| EDA | Kafka, Events | Entkopplung & Resilienz | Asynchrone Kommunikation |
| KMP | Kotlin Multiplatform | Effizienz & Konsistenz | Shared Business Logic |
## 🔧 Backend-Entwicklungsrichtlinien
### Microservice-Struktur (Clean Architecture)
Jeder fachliche Microservice folgt der 4-Layer-Struktur (`api`, `application`, `domain`, `infrastructure`).
```
service-name/
├── service-name-api/ # REST-Endpoints, DTOs
├── service-name-application/ # Use Cases, Commands, Queries
├── service-name-domain/ # Domain Models, Events, Services
└── service-name-infrastructure/ # Repositories, External Services
```
#### Layer-Verantwortlichkeiten
**API Layer (`-api`):**
```kotlin
@RestController
@RequestMapping("/api/v1/members")
class MemberController(
private val memberService: MemberService
) {
@PostMapping
fun createMember(@RequestBody request: CreateMemberRequest): ResponseEntity<MemberResponse> {
val command = CreateMemberCommand(
name = request.name,
email = request.email,
licenseNumber = request.licenseNumber
)
return when (val result = memberService.createMember(command)) {
is Result.Success -> ResponseEntity.ok(result.value.toResponse())
is Result.Failure -> ResponseEntity.badRequest().body(result.error.toErrorResponse())
}
}
}
```
**Application Layer (`-application`):**
```kotlin
@Service
class MemberService(
private val memberRepository: MemberRepository,
private val eventPublisher: EventPublisher
) {
suspend fun createMember(command: CreateMemberCommand): Result<Member, BusinessError> {
// Validation
val validationResult = validateCreateMemberCommand(command)
if (validationResult is Result.Failure) {
return validationResult
}
// Business Logic
val member = Member.create(
name = command.name,
email = command.email,
licenseNumber = command.licenseNumber
)
// Persistence
return memberRepository.save(member).map {
// Event Publishing
eventPublisher.publish(MemberCreatedEvent(member))
member
}
}
}
```
**Domain Layer (`-domain`):**
```kotlin
@JvmInline
value class MemberId(val value: UUID) {
companion object {
fun generate(): MemberId = MemberId(UUID.randomUUID())
}
}
data class Member private constructor(
val id: MemberId,
val name: String,
val email: Email,
val licenseNumber: LicenseNumber,
val status: MemberStatus = MemberStatus.PENDING
) {
companion object {
fun create(
name: String,
email: String,
licenseNumber: String
): Result<Member, ValidationError> {
return Result.Success(
Member(
id = MemberId.generate(),
name = name,
email = Email.of(email).getOrThrow(),
licenseNumber = LicenseNumber.of(licenseNumber).getOrThrow()
)
)
}
}
fun activate(): Member = copy(status = MemberStatus.ACTIVE)
fun suspend(): Member = copy(status = MemberStatus.SUSPENDED)
}
```
**Infrastructure Layer (`-infrastructure`):**
```kotlin
@Repository
class PostgresMemberRepository(
private val jdbcTemplate: JdbcTemplate
) : MemberRepository {
override suspend fun save(member: Member): Result<Unit, RepositoryError> {
return try {
jdbcTemplate.update(
"INSERT INTO members (id, name, email, license_number, status) VALUES (?, ?, ?, ?, ?)",
member.id.value,
member.name,
member.email.value,
member.licenseNumber.value,
member.status.name
)
Result.Success(Unit)
} catch (e: DataAccessException) {
Result.Failure(RepositoryError.DATABASE_ERROR)
}
}
override suspend fun findById(id: MemberId): Result<Member?, RepositoryError> {
return try {
val member = jdbcTemplate.queryForObject(
"SELECT * FROM members WHERE id = ?",
arrayOf(id.value)
) { rs, _ ->
Member(
id = MemberId(UUID.fromString(rs.getString("id"))),
name = rs.getString("name"),
email = Email.of(rs.getString("email")).getOrThrow(),
licenseNumber = LicenseNumber.of(rs.getString("license_number")).getOrThrow(),
status = MemberStatus.valueOf(rs.getString("status"))
)
}
Result.Success(member)
} catch (e: EmptyResultDataAccessException) {
Result.Success(null)
} catch (e: DataAccessException) {
Result.Failure(RepositoryError.DATABASE_ERROR)
}
}
}
```
### Repository-Pattern
Jede Repository-Methode muss das `Result`-Pattern verwenden.
```kotlin
interface MemberRepository {
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
suspend fun save(member: Member): Result<Unit, RepositoryError>
suspend fun findByEmail(email: Email): Result<Member?, RepositoryError>
suspend fun findByLicenseNumber(licenseNumber: LicenseNumber): Result<Member?, RepositoryError>
suspend fun findAll(pageable: Pageable): Result<Page<Member>, RepositoryError>
}
```
### Messaging & Event-Naming
Event-Naming Convention: Domänen-Events folgen dem Muster `{Domain}{Entity}{Action}Event`.
```kotlin
data class MemberPersonalDataUpdatedEvent(
val memberId: MemberId,
val oldName: String,
val newName: String,
val oldEmail: Email,
val newEmail: Email,
val updatedAt: Instant = Instant.now(),
val correlationId: String = MDC.get("correlationId") ?: UUID.randomUUID().toString()
) : DomainEvent {
override val eventType: String = "member.personal-data.updated"
override val aggregateId: String = memberId.value.toString()
override val version: Int = 1
}
```
## 📱 Frontend-Entwicklungsrichtlinien
Das Frontend folgt konsequent dem **Model-View-ViewModel (MVVM)**-Muster und der **Kotlin Multiplatform (KMP)**-Strategie. Der UI-Code wird nach **fachlichen Features** (vertikale Schnitte) strukturiert.
### Multiplatform-Struktur
```
client/
├── src/commonMain/kotlin/ # Shared Business Logic
│ ├── domain/ # Domain Models
│ ├── data/ # Repositories, API-Clients
│ ├── presentation/ # ViewModels, UI-States
│ └── ui/ # Shared UI-Components
├── src/jvmMain/kotlin/ # Desktop-spezifischer Code
│ └── ui/ # Desktop UI-Adaptierungen
└── src/wasmJsMain/kotlin/ # Web-spezifischer Code
└── ui/ # Web UI-Adaptierungen
```
### MVVM-Implementation
**Shared ViewModel (commonMain):**
```kotlin
class MemberListViewModel(
private val memberRepository: MemberRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(MemberListUiState())
val uiState: StateFlow<MemberListUiState> = _uiState.asStateFlow()
fun loadMembers() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
when (val result = memberRepository.getAllMembers()) {
is Result.Success -> {
_uiState.value = _uiState.value.copy(
isLoading = false,
members = result.value,
error = null
)
}
is Result.Failure -> {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = result.error.message
)
}
}
}
}
}
data class MemberListUiState(
val isLoading: Boolean = false,
val members: List<Member> = emptyList(),
val error: String? = null
)
```
**Shared UI-Component (commonMain):**
```kotlin
@Composable
fun MemberListScreen(
viewModel: MemberListViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadMembers()
}
Column {
if (uiState.isLoading) {
CircularProgressIndicator()
}
uiState.error?.let { error ->
Text(
text = error,
color = MaterialTheme.colorScheme.error
)
}
LazyColumn {
items(uiState.members) { member ->
MemberCard(
member = member,
onMemberClick = { /* Handle click */ }
)
}
}
}
}
```
## 🎯 Domain-Driven Design (DDD) Patterns
### Bounded Contexts
```
Meldestelle-Domain/
├── member-context/ # Mitgliederverwaltung
├── tournament-context/ # Turnierverwaltung
├── horse-context/ # Pferdeverwaltung
├── registration-context/ # Anmeldungen
└── payment-context/ # Zahlungsabwicklung
```
### Aggregate Design
```kotlin
class Tournament private constructor(
val id: TournamentId,
val name: String,
val startDate: LocalDate,
val endDate: LocalDate,
val maxParticipants: Int,
private val registrations: MutableList<TournamentRegistration> = mutableListOf()
) {
companion object {
fun create(
name: String,
startDate: LocalDate,
endDate: LocalDate,
maxParticipants: Int
): Result<Tournament, ValidationError> {
// Business rules validation
if (startDate.isAfter(endDate)) {
return Result.Failure(ValidationError.INVALID_DATE_RANGE)
}
return Result.Success(
Tournament(
id = TournamentId.generate(),
name = name,
startDate = startDate,
endDate = endDate,
maxParticipants = maxParticipants
)
)
}
}
fun registerMember(memberId: MemberId): Result<TournamentRegistrationCreatedEvent, BusinessError> {
// Business rules
if (registrations.size >= maxParticipants) {
return Result.Failure(BusinessError.TOURNAMENT_FULL)
}
if (registrations.any { it.memberId == memberId }) {
return Result.Failure(BusinessError.ALREADY_REGISTERED)
}
val registration = TournamentRegistration(
id = TournamentRegistrationId.generate(),
tournamentId = id,
memberId = memberId,
registrationDate = LocalDateTime.now()
)
registrations.add(registration)
return Result.Success(
TournamentRegistrationCreatedEvent(
tournamentId = id,
memberId = memberId,
registrationId = registration.id
)
)
}
}
```
### Infrastructure & Betrieb
#### Kafka-Konfiguration
Die Konfiguration muss auf maximale Zuverlässigkeit ausgelegt sein:
```yaml
# application.yml
kafka:
producer:
acks: all
enable-idempotence: true
max-in-flight-requests-per-connection: 1
consumer:
group-id-prefix: "meldestelle-${spring.application.name}"
auto-offset-reset: earliest
enable-auto-commit: false
```
#### Datenbank-Migrationen (Flyway)
Migrations-Skripte müssen einer klaren Namenskonvention folgen:
* **Pattern:** `V{version}__{description}.sql` (z.B., `V001__Create_member_tables.sql`)
* **Repeatable:** `R__{description}.sql` (z.B., `R__Update_member_view.sql`)
#### API-Dokumentation (OpenAPI)
Alle öffentlichen REST-Endpunkte müssen mit OpenAPI-Annotationen (`@Operation`, `@ApiResponse`) dokumentiert werden, um eine klare und interaktive API-Dokumentation zu generieren.
```kotlin
@Operation(
summary = "Neues Mitglied erstellen",
description = "Erstellt ein neues Mitglied mit den angegebenen Daten"
)
@ApiResponses(
value = [
ApiResponse(
responseCode = "201",
description = "Mitglied erfolgreich erstellt"
),
ApiResponse(
responseCode = "400",
description = "Ungültige Eingabedaten"
)
]
)
@PostMapping
fun createMember(@RequestBody request: CreateMemberRequest): ResponseEntity<MemberResponse>
```
## 🚀 Architektur-Entscheidungen (ADRs)
### ADR-001: Microservices mit Domain-Driven Design
**Status:** Akzeptiert
**Kontext:** Skalierbare und wartbare Architektur für Pferdesport-Plattform
**Entscheidung:** Microservices-Architektur mit DDD-Bounded-Contexts
**Konsequenzen:**
- ✅ Unabhängige Entwicklung und Deployment
- ✅ Fachliche Kapselung durch Bounded Contexts
- ❌ Komplexität bei Service-zu-Service-Kommunikation
- ❌ Eventual Consistency zwischen Services
### ADR-002: Event-Driven Architecture mit Kafka
**Status:** Akzeptiert
**Kontext:** Entkopplung und Resilienz zwischen Services
**Entscheidung:** Kafka als zentraler Event-Broker
**Konsequenzen:**
- ✅ Lose Kopplung zwischen Services
- ✅ Audit-Log durch Event-Store
- ❌ Komplexität bei Event-Schema-Evolution
- ❌ Eventually Consistent State
### ADR-003: Kotlin Multiplatform für Client
**Status:** Akzeptiert
**Kontext:** Code-Sharing zwischen Desktop und Web
**Entscheidung:** KMP mit Compose Multiplatform
**Konsequenzen:**
- ✅ Geteilte Business-Logic
- ✅ Einheitliche UI-Patterns
- ❌ Plattform-spezifische Optimierungen schwieriger
- ❌ Abhängigkeit von Kotlin/JetBrains-Ökosystem
---
**Navigation:**
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
- [Coding-Standards](./coding-standards.md) - Code-Qualitätsstandards
- [Testing-Standards](./testing-standards.md) - Test-Qualitätssicherung
- [Documentation-Standards](./documentation-standards.md) - Dokumentationsrichtlinien
@@ -0,0 +1,216 @@
# Coding Standards und Code-Qualität
---
guideline_type: "project-standards"
scope: "coding-standards"
audience: ["developers", "ai-assistants"]
last_updated: "2025-09-13"
dependencies: ["master-guideline.md"]
related_files: ["build.gradle.kts", "detekt.yml", "*.kt"]
ai_context: "Coding conventions, naming standards, type safety, error handling, and logging practices"
---
## 📋 Coding Conventions & Code-Qualität
### Sprach- und Stilstandards
* **Primärsprache:** Kotlin (JVM/Multiplatform)
* **Java-Kompatibilität:** Ziel ist Java 21+
* **Code-Stil:** Offizielle Kotlin Coding Conventions, durch `Detekt` geprüft.
> **🤖 AI-Assistant Hinweis:**
> Alle Kotlin-Code muss den offiziellen Kotlin Coding Conventions entsprechen:
> - **Detekt-Validierung:** Automatische Code-Style-Prüfung
> - **Java 21+ Kompatibilität:** Nutze moderne Java-Features wo sinnvoll
> - **Multiplatform:** Code sollte plattformübergreifend funktionieren
### Namenskonventionen
* **Klassen & Interfaces:** `PascalCase` (z.B. `MemberService`, `EventRepository`)
* **Funktionen & Variablen:** `camelCase` (z.B. `authenticateUser`, `memberRepository`)
* **Testmethoden:** Beschreibend mit Backticks (z.B. `` `should return Success for valid credentials` ``)
* **Konstanten:** `SCREAMING_SNAKE_CASE` (z.B. `MAX_RETRY_ATTEMPTS`)
* **Enums:** `PascalCase` für Werte (z.B. `MemberStatus.ACTIVE`)
### Value Classes für Typsicherheit
Primitive Typen (UUID, String, Long) für IDs oder spezifische Werte müssen in typsichere `value class`-Wrapper gekapselt werden.
```kotlin
@JvmInline
value class MemberId(val value: UUID) {
companion object {
fun of(value: String): Result<MemberId, ValidationError> =
runCatching { UUID.fromString(value) }
.map { MemberId(it) }
.mapError { ValidationError.INVALID_UUID }
}
}
```
### Error-Handling & Logging
* **`Result`-Pattern:** Für erwartbare Geschäftsfehler ist das `Result`-Pattern zu verwenden. Exceptions sind für unerwartete, technische Fehler reserviert.
* **Fehler-Hierarchie:** Wir verwenden eine `sealed class`-Hierarchie, um Fehlerarten klar zu kategorisieren (`DomainError`, `ValidationError`, `BusinessError`, `TechnicalError`).
* **Structured Logging:** Logs müssen strukturiert sein und eine Korrelations-ID enthalten, um Anfragen über Service-Grenzen hinweg zu verfolgen.
```kotlin
logger.info {
"Creating member" with mapOf(
"memberId" to command.memberId.value,
"correlationId" to MDC.get("correlationId")
)
}
```
## 🎯 AI-Assistenten: Coding-Standards-Schnellreferenz
### Namenskonventionen-Übersicht
| Element | Convention | Beispiel |
|---------|------------|----------|
| Klassen/Interfaces | PascalCase | `MemberService`, `EventRepository` |
| Funktionen/Variablen | camelCase | `authenticateUser`, `memberRepository` |
| Konstanten | SCREAMING_SNAKE_CASE | `MAX_RETRY_ATTEMPTS` |
| Test-Methoden | Backticks beschreibend | `` `should return Success for valid credentials` `` |
| Enum-Werte | PascalCase | `MemberStatus.ACTIVE` |
### Code-Qualitäts-Checkliste
- [ ] **Detekt-Prüfung:** Code-Stil entspricht Kotlin Conventions
- [ ] **Value Classes:** Primitive Typen sind in typsichere Wrapper gekapselt
- [ ] **Result-Pattern:** Geschäftsfehler verwenden Result statt Exceptions
- [ ] **Structured Logging:** Logs enthalten Korrelations-IDs
- [ ] **Error-Hierarchie:** Sealed Classes für Fehlerkategorisierung
### Häufige Code-Patterns
#### Typsichere IDs
```kotlin
@JvmInline
value class EntityId(val value: UUID) {
companion object {
fun generate(): EntityId = EntityId(UUID.randomUUID())
fun of(value: String): Result<EntityId, ValidationError> =
runCatching { UUID.fromString(value) }
.map { EntityId(it) }
.mapError { ValidationError.INVALID_UUID }
}
}
```
#### Error-Handling mit Result
```kotlin
interface EntityRepository {
suspend fun findById(id: EntityId): Result<Entity?, RepositoryError>
suspend fun save(entity: Entity): Result<Unit, RepositoryError>
}
// Verwendung
when (val result = repository.findById(entityId)) {
is Result.Success -> processEntity(result.value)
is Result.Failure -> handleError(result.error)
}
```
#### Structured Logging
```kotlin
class EntityService {
private val logger = LoggerFactory.getLogger(EntityService::class.java)
suspend fun processEntity(command: ProcessEntityCommand): Result<Unit, ProcessingError> {
val correlationId = MDC.get("correlationId")
logger.info {
"Processing entity" with mapOf(
"entityId" to command.entityId.value,
"correlationId" to correlationId,
"operation" to "process"
)
}
return try {
// Processing logic
Result.Success(Unit)
} catch (e: Exception) {
logger.error {
"Entity processing failed" with mapOf(
"entityId" to command.entityId.value,
"correlationId" to correlationId,
"error" to e.message
)
}
Result.Failure(ProcessingError.TECHNICAL_ERROR)
}
}
}
```
#### Sealed Class Hierarchie für Fehler
```kotlin
sealed interface DomainError {
val message: String
val code: String
}
sealed interface ValidationError : DomainError {
data object INVALID_UUID : ValidationError {
override val message = "Invalid UUID format"
override val code = "VALIDATION_INVALID_UUID"
}
data object REQUIRED_FIELD_MISSING : ValidationError {
override val message = "Required field is missing"
override val code = "VALIDATION_REQUIRED_FIELD_MISSING"
}
}
sealed interface BusinessError : DomainError {
data object ENTITY_NOT_FOUND : BusinessError {
override val message = "Entity not found"
override val code = "BUSINESS_ENTITY_NOT_FOUND"
}
}
sealed interface TechnicalError : DomainError {
data object DATABASE_CONNECTION_FAILED : TechnicalError {
override val message = "Database connection failed"
override val code = "TECHNICAL_DATABASE_CONNECTION_FAILED"
}
}
```
### Detekt-Konfiguration
Wichtige Detekt-Regeln für das Projekt:
```yaml
# detekt.yml
style:
MaxLineLength:
maxLineLength: 120
FunctionNaming:
functionPattern: '^[a-z][a-zA-Z0-9]*$'
ClassNaming:
classPattern: '^[A-Z][a-zA-Z0-9]*$'
complexity:
ComplexMethod:
threshold: 15
LongParameterList:
functionThreshold: 6
potential-bugs:
UnsafeCallOnNullableType:
active: true
```
---
**Navigation:**
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
- [Testing-Standards](./testing-standards.md) - Test-Qualitätsstandards
- [Documentation-Standards](./documentation-standards.md) - Dokumentationsrichtlinien
- [Architecture-Principles](./architecture-principles.md) - Architektur-Grundsätze
@@ -0,0 +1,326 @@
# Documentation Standards
---
guideline_type: "project-standards"
scope: "documentation-standards"
audience: ["developers", "ai-assistants", "technical-writers"]
last_updated: "2025-09-13"
dependencies: ["master-guideline.md"]
related_files: ["README*.md", "docs/**", "*.md", "openapi.yaml"]
ai_context: "Documentation language standards, README structure, API documentation, and technical writing guidelines"
---
## 📝 Dokumentationsstandards
### Sprache für Dokumentation
* **README-Dateien:** Alle README-Dokumentationen im Projekt müssen in **deutscher Sprache** verfasst werden. Dies gewährleistet Konsistenz und Zugänglichkeit für das deutsche Entwicklungsteam.
* **Code-Kommentare:** Komplexe Geschäftslogik und fachliche Zusammenhänge sollen in deutscher Sprache kommentiert werden.
* **API-Dokumentation:** OpenAPI-Beschreibungen und -Beispiele sind bevorzugt in deutscher Sprache zu verfassen, sofern keine internationalen Anforderungen bestehen.
> **🤖 AI-Assistant Hinweis:**
> Dokumentationssprache-Regeln:
> - **README-Dateien:** Immer Deutsch
> - **Code-Kommentare:** Deutsch für Geschäftslogik, Englisch für technische Details
> - **API-Docs:** Deutsch bevorzugt, Englisch bei internationalen APIs
> - **Technische Begriffe:** Englische Originalform wenn keine deutsche Übersetzung etabliert
### Dokumentationsstruktur
* README-Dateien sollen eine einheitliche Struktur befolgen: Überblick, Architektur, Entwicklung, Tests, Deployment.
* Technische Begriffe dürfen in englischer Originalform verwendet werden, wenn keine etablierte deutsche Übersetzung existiert.
## 🎯 AI-Assistenten: Documentation-Schnellreferenz
### README-Template-Struktur
```markdown
# [Projekt/Modul Name]
## Überblick
[Kurze Beschreibung des Zwecks und der Funktionalität]
## Architektur
[Architektonische Entscheidungen und Komponenten-Übersicht]
## Entwicklung
[Setup-Anweisungen für lokale Entwicklung]
### Voraussetzungen
[Erforderliche Tools und Versionen]
### Installation
[Schritt-für-Schritt Setup-Anleitung]
### Konfiguration
[Wichtige Konfigurationsoptionen]
## Tests
[Test-Ausführung und Test-Strategie]
## Deployment
[Deployment-Anweisungen für verschiedene Umgebungen]
## API-Dokumentation
[Links zu API-Docs oder eingebettete Dokumentation]
## Troubleshooting
[Häufige Probleme und Lösungen]
```
### Code-Kommentar-Standards
#### Deutsche Geschäftslogik-Kommentare
```kotlin
/**
* Prüft, ob ein Mitglied für die Anmeldung zu einem Turnier berechtigt ist.
*
* Ein Mitglied ist berechtigt, wenn:
* - Der Mitgliedsstatus AKTIV ist
* - Die Lizenz gültig und nicht suspendiert ist
* - Keine offenen Zahlungen vorliegen
*/
fun isEligibleForTournament(member: Member, tournament: Tournament): Result<Boolean, ValidationError> {
// Mitgliedsstatus prüfen
if (member.status != MemberStatus.ACTIVE) {
return Result.Failure(ValidationError.MEMBER_NOT_ACTIVE)
}
// Lizenzvalidierung durchführen
return validateLicense(member, tournament)
}
```
#### Englische technische Kommentare
```kotlin
/**
* Cache implementation using Redis with TTL support
* Performance: O(1) for get/set operations
*/
class RedisCache<T>(
private val redisClient: RedisClient,
private val ttl: Duration = Duration.ofHours(1)
) : Cache<T> {
override suspend fun get(key: String): T? {
// Use Redis GET command with automatic deserialization
return redisClient.get(key)?.let {
jsonMapper.readValue(it, typeRef<T>())
}
}
}
```
### OpenAPI-Dokumentation Standards
#### Deutsche API-Beschreibungen
```yaml
openapi: 3.0.0
info:
title: Meldestelle API
description: REST API für die Verwaltung von Pferdesport-Meldungen
version: 1.0.0
paths:
/members:
post:
summary: Neues Mitglied anlegen
description: |
Erstellt ein neues Mitglied in der Datenbank.
Validiert alle Pflichtfelder und prüft auf Duplikate.
requestBody:
description: Mitgliedsdaten für die Erstellung
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateMemberRequest'
example:
name: "Max Mustermann"
email: "max.mustermann@example.com"
licenseNumber: "12345"
responses:
'201':
description: Mitglied erfolgreich erstellt
content:
application/json:
schema:
$ref: '#/components/schemas/Member'
'400':
description: Ungültige Eingabedaten
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
components:
schemas:
Member:
type: object
description: Repräsentiert ein Mitglied im System
properties:
id:
type: string
format: uuid
description: Eindeutige Mitglieds-ID
example: "550e8400-e29b-41d4-a716-446655440000"
name:
type: string
description: Vollständiger Name des Mitglieds
example: "Max Mustermann"
email:
type: string
format: email
description: E-Mail-Adresse des Mitglieds
example: "max.mustermann@example.com"
```
### Dokumentations-Checkliste
#### README-Dateien
- [ ] **Struktur:** Folgt dem Standard-Template
- [ ] **Sprache:** Auf Deutsch verfasst
- [ ] **Aktualität:** Entspricht dem aktuellen Code-Stand
- [ ] **Vollständigkeit:** Alle erforderlichen Abschnitte vorhanden
- [ ] **Beispiele:** Konkrete Code-Beispiele und Kommandos
- [ ] **Links:** Funktionierende Verweise auf verwandte Dokumentation
#### API-Dokumentation
- [ ] **OpenAPI-Spezifikation:** Vollständig und valide
- [ ] **Deutsche Beschreibungen:** Für alle Endpunkte und Schemas
- [ ] **Beispiele:** Realistische Request/Response-Beispiele
- [ ] **Error-Handling:** Dokumentierte Fehlerfälle
- [ ] **Authentifizierung:** Sicherheitsanforderungen dokumentiert
#### Code-Kommentare
- [ ] **Geschäftslogik:** Deutsche Kommentare für fachliche Aspekte
- [ ] **Technische Details:** Englische Kommentare für Framework-/Library-Code
- [ ] **Komplexität:** Komplexe Algorithmen sind erklärt
- [ ] **TODOs:** Mit Ticket-Referenzen versehen
- [ ] **Javadoc/KDoc:** Für öffentliche APIs vollständig
### Dokumentations-Patterns
#### Architektur-Diagramme
```markdown
## System-Architektur
```mermaid
graph TB
subgraph "Client Layer"
WEB[Web App]
MOBILE[Mobile App]
end
subgraph "API Gateway"
GW[API Gateway]
end
subgraph "Service Layer"
MS[Member Service]
TS[Tournament Service]
NS[Notification Service]
end
subgraph "Data Layer"
PG[(PostgreSQL)]
RD[(Redis)]
end
WEB --> GW
MOBILE --> GW
GW --> MS
GW --> TS
GW --> NS
MS --> PG
TS --> PG
NS --> RD
```
```
#### Feature-Dokumentation
```markdown
## Feature: Turnier-Anmeldung
### Fachlicher Überblick
Die Turnier-Anmeldung ermöglicht es Mitgliedern, sich für Turniere zu registrieren.
### User Stories
- Als Mitglied möchte ich mich für ein Turnier anmelden können
- Als Turnierleiter möchte ich Anmeldungen verwalten können
### Technische Umsetzung
#### API-Endpunkte
- `POST /tournaments/{id}/registrations` - Anmeldung erstellen
- `GET /tournaments/{id}/registrations` - Anmeldungen abrufen
- `DELETE /registrations/{id}` - Anmeldung stornieren
#### Domain-Events
- `TournamentRegistrationCreated` - Bei erfolgreicher Anmeldung
- `TournamentRegistrationCancelled` - Bei Stornierung
### Business Rules
1. Anmeldung nur für aktive Mitglieder möglich
2. Anmeldeschluss muss beachtet werden
3. Maximale Teilnehmerzahl darf nicht überschritten werden
```
#### Troubleshooting-Dokumentation
```markdown
## Häufige Probleme
### Problem: Service startet nicht
**Symptome:** Container bleibt im Status "Restarting"
**Ursachen:**
- Datenbankverbindung fehlgeschlagen
- Fehlende Environment-Variablen
- Port bereits belegt
**Lösung:**
1. Logs prüfen: `docker-compose logs service-name`
2. Environment-Variablen validieren
3. Port-Konflikte lösen: `netstat -tulpn | grep :8080`
### Problem: Langsame API-Antworten
**Symptome:** Response-Zeiten > 2 Sekunden
**Debugging:**
```bash
# Database-Performance prüfen
docker-compose exec postgres psql -c "SELECT * FROM pg_stat_activity;"
# Redis-Performance prüfen
docker-compose exec redis redis-cli info stats
```
**Optimierung:**
- Database-Indizes überprüfen
- Query-Performance analysieren
- Cache-Hit-Rate optimieren
```
### Versionierung und Updates
#### Dokumentations-Versionierung
- README-Dateien werden mit dem Code versioniert
- API-Dokumentation folgt Semantic Versioning
- Changelog wird für breaking changes geführt
#### Update-Prozess
1. **Code-Änderungen** → README aktualisieren
2. **API-Änderungen** → OpenAPI-Spec anpassen
3. **Architektur-Änderungen** → Diagramme überarbeiten
4. **Deployment-Änderungen** → Deployment-Docs aktualisieren
---
**Navigation:**
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
- [Coding-Standards](./coding-standards.md) - Code-Qualitätsstandards
- [Testing-Standards](./testing-standards.md) - Test-Qualitätssicherung
- [Architecture-Principles](./architecture-principles.md) - Architektur-Grundsätze
@@ -0,0 +1,379 @@
# Testing Standards und Qualitätssicherung
---
guideline_type: "project-standards"
scope: "testing-standards"
audience: ["developers", "ai-assistants"]
last_updated: "2025-09-13"
dependencies: ["master-guideline.md", "coding-standards.md"]
related_files: ["build.gradle.kts", "src/test/**", "testcontainers.properties"]
ai_context: "Testing strategies, test pyramid, tools, coverage requirements, and debugging practices"
---
## 🧪 Testing Standards
Tests sind ein integraler Bestandteil jedes Features und müssen einen hohen Standard erfüllen.
> **🤖 AI-Assistant Hinweis:**
> Testing-Prinzipien für das Meldestelle-Projekt:
> - **Test-Pyramide:** 80%+ Unit-Tests, Integrationstests für externe Systeme
> - **Testcontainers:** Goldstandard für Infrastruktur-Tests
> - **Debug-Logs:** Präfix `[DEBUG_LOG]` für Test-Ausgaben
> - **Result-Pattern:** Tests müssen auch Error-Handling validieren
### Test-Pyramide & Werkzeuge
#### Unit-Tests (80 %+ Abdeckung)
Für Domänen- und Anwendungslogik (JUnit 5, MockK).
```kotlin
class MemberServiceTest {
private val memberRepository = mockk<MemberRepository>()
private val eventPublisher = mockk<EventPublisher>()
private val memberService = MemberService(memberRepository, eventPublisher)
@Test
fun `should return Success when member is created successfully`() {
// Given
val command = CreateMemberCommand(
memberId = MemberId.generate(),
name = "Max Mustermann",
email = "max@example.com"
)
every { memberRepository.save(any()) } returns Result.Success(Unit)
every { eventPublisher.publish(any()) } returns Result.Success(Unit)
// When
val result = memberService.createMember(command)
// Then
assertThat(result).isInstanceOf<Result.Success<Unit>>()
verify { memberRepository.save(any()) }
verify { eventPublisher.publish(ofType<MemberCreatedEvent>()) }
}
@Test
fun `should return Failure when repository save fails`() {
// Given
val command = CreateMemberCommand(
memberId = MemberId.generate(),
name = "Max Mustermann",
email = "max@example.com"
)
every { memberRepository.save(any()) } returns Result.Failure(RepositoryError.DATABASE_ERROR)
// When
val result = memberService.createMember(command)
// Then
assertThat(result).isInstanceOf<Result.Failure<RepositoryError>>()
verify { memberRepository.save(any()) }
verify(exactly = 0) { eventPublisher.publish(any()) }
}
}
```
#### Integrationstests
Decken alle Repository-Implementierungen und externen Integrationen ab.
```kotlin
@Testcontainers
class MemberRepositoryIntegrationTest {
@Container
private val postgresContainer = PostgreSQLContainer("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
private lateinit var memberRepository: MemberRepository
@BeforeEach
fun setup() {
val dataSource = HikariDataSource().apply {
jdbcUrl = postgresContainer.jdbcUrl
username = postgresContainer.username
password = postgresContainer.password
}
// Run migrations
Flyway.configure()
.dataSource(dataSource)
.locations("db/migration")
.load()
.migrate()
memberRepository = PostgresMemberRepository(dataSource)
}
@Test
fun `should save and retrieve member successfully`() {
// Given
val member = Member(
id = MemberId.generate(),
name = "Integration Test Member",
email = "integration@test.com"
)
// When
val saveResult = runBlocking { memberRepository.save(member) }
val findResult = runBlocking { memberRepository.findById(member.id) }
// Then
assertThat(saveResult).isInstanceOf<Result.Success<Unit>>()
assertThat(findResult).isInstanceOf<Result.Success<Member?>>()
val retrievedMember = (findResult as Result.Success).value
assertThat(retrievedMember?.id).isEqualTo(member.id)
assertThat(retrievedMember?.name).isEqualTo(member.name)
assertThat(retrievedMember?.email).isEqualTo(member.email)
}
}
```
#### Testcontainers als Goldstandard
Jede Interaktion mit externer Infrastruktur (DB, Cache, Broker) **muss** mit **Testcontainers** getestet werden.
```kotlin
@Testcontainers
class EventStoreIntegrationTest {
companion object {
@Container
@JvmStatic
private val redisContainer = GenericContainer<Nothing>("redis:7-alpine")
.withExposedPorts(6379)
@Container
@JvmStatic
private val kafkaContainer = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
}
@Test
fun `should store and retrieve events from Redis`() {
println("[DEBUG_LOG] Testing Redis event storage")
// Given
val eventStore = RedisEventStore(
redisHost = redisContainer.host,
redisPort = redisContainer.getMappedPort(6379)
)
val event = MemberCreatedEvent(
memberId = MemberId.generate(),
name = "Test Member",
timestamp = Instant.now()
)
// When
val storeResult = runBlocking { eventStore.store(event) }
val retrieveResult = runBlocking { eventStore.getEvents(event.memberId) }
// Then
assertThat(storeResult).isInstanceOf<Result.Success<Unit>>()
assertThat(retrieveResult).isInstanceOf<Result.Success<List<DomainEvent>>>()
val events = (retrieveResult as Result.Success).value
assertThat(events).hasSize(1)
assertThat(events.first()).isInstanceOf<MemberCreatedEvent>()
println("[DEBUG_LOG] Successfully stored and retrieved ${events.size} events")
}
}
```
### Debugging in Tests
Debug-Ausgaben im Test-Code müssen mit `[DEBUG_LOG]` beginnen, um sie leicht identifizieren und filtern zu können.
```kotlin
@Test
fun `should handle complex business scenario`() {
println("[DEBUG_LOG] Starting complex business scenario test")
// Test implementation
println("[DEBUG_LOG] Member created with ID: ${member.id}")
println("[DEBUG_LOG] Published ${events.size} domain events")
println("[DEBUG_LOG] Test completed successfully")
}
```
## 🎯 AI-Assistenten: Testing-Schnellreferenz
### Test-Kategorien und Werkzeuge
| Test-Typ | Coverage-Ziel | Werkzeuge | Verwendung |
|----------|---------------|-----------|------------|
| Unit-Tests | 80%+ | JUnit 5, MockK, AssertJ | Domänen- & Anwendungslogik |
| Integrationstests | Alle Repositories | Testcontainers, JUnit 5 | Externe Integrationen |
| End-to-End Tests | Kritische User-Journeys | Testcontainers, REST Assured | Vollständige Workflows |
### Testcontainer-Konfiguration
#### PostgreSQL
```kotlin
@Container
private val postgresContainer = PostgreSQLContainer("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withInitScript("test-data.sql")
```
#### Redis
```kotlin
@Container
private val redisContainer = GenericContainer<Nothing>("redis:7-alpine")
.withExposedPorts(6379)
.withCommand("redis-server", "--appendonly", "yes")
```
#### Kafka
```kotlin
@Container
private val kafkaContainer = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
.withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true")
```
#### Keycloak
```kotlin
@Container
private val keycloakContainer = KeycloakContainer("quay.io/keycloak/keycloak:26.0.7")
.withRealmImportFile("test-realm.json")
.withAdminUsername("admin")
.withAdminPassword("admin")
```
### Test-Patterns für Result-Handling
```kotlin
// Success-Case testen
@Test
fun `should return Success when operation succeeds`() {
// Given
every { dependency.operation() } returns Result.Success(expectedValue)
// When
val result = serviceUnderTest.performOperation()
// Then
assertThat(result).isInstanceOf<Result.Success<ExpectedType>>()
assertThat((result as Result.Success).value).isEqualTo(expectedValue)
}
// Failure-Case testen
@Test
fun `should return Failure when dependency fails`() {
// Given
every { dependency.operation() } returns Result.Failure(ExpectedError.SOME_ERROR)
// When
val result = serviceUnderTest.performOperation()
// Then
assertThat(result).isInstanceOf<Result.Failure<ExpectedError>>()
assertThat((result as Result.Failure).error).isEqualTo(ExpectedError.SOME_ERROR)
}
```
### Mock-Setup für Services
```kotlin
class ServiceTest {
private val repository = mockk<Repository>()
private val eventPublisher = mockk<EventPublisher>()
private val externalService = mockk<ExternalService>()
private val serviceUnderTest = Service(repository, eventPublisher, externalService)
@BeforeEach
fun setup() {
clearAllMocks()
// Default mocks
every { eventPublisher.publish(any()) } returns Result.Success(Unit)
}
@AfterEach
fun cleanup() {
confirmVerified(repository, eventPublisher, externalService)
}
}
```
### Testdaten-Builder
```kotlin
class MemberTestDataBuilder {
private var id: MemberId = MemberId.generate()
private var name: String = "Test Member"
private var email: String = "test@example.com"
private var status: MemberStatus = MemberStatus.ACTIVE
fun withId(id: MemberId) = apply { this.id = id }
fun withName(name: String) = apply { this.name = name }
fun withEmail(email: String) = apply { this.email = email }
fun withStatus(status: MemberStatus) = apply { this.status = status }
fun build() = Member(
id = id,
name = name,
email = email,
status = status
)
}
// Verwendung in Tests
@Test
fun `should validate member data`() {
val member = MemberTestDataBuilder()
.withName("Max Mustermann")
.withEmail("max@meldestelle.at")
.withStatus(MemberStatus.PENDING)
.build()
// Test implementation
}
```
### Performance-Tests
```kotlin
@Test
fun `should handle high load efficiently`() {
println("[DEBUG_LOG] Starting performance test with 1000 concurrent operations")
val operations = (1..1000).map {
async {
serviceUnderTest.performOperation(
TestCommand(id = MemberId.generate())
)
}
}
val results = runBlocking {
operations.awaitAll()
}
val successCount = results.count { it is Result.Success }
val failureCount = results.count { it is Result.Failure }
println("[DEBUG_LOG] Performance test completed: $successCount successes, $failureCount failures")
assertThat(successCount).isGreaterThan(950) // 95% success rate minimum
}
```
---
**Navigation:**
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
- [Coding-Standards](./coding-standards.md) - Code-Qualitätsstandards
- [Documentation-Standards](./documentation-standards.md) - Dokumentationsrichtlinien
- [Architecture-Principles](./architecture-principles.md) - Architektur-Grundsätze