feature(guideline)
This commit is contained in:
@@ -1,464 +0,0 @@
|
||||
# Meldestelle Development Guidelines
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** 2025-08-15
|
||||
**Status:** Active
|
||||
|
||||
This document outlines the development guidelines for the Meldestelle project, covering coding conventions, code organization, and testing approaches.
|
||||
|
||||
---
|
||||
|
||||
## 1. Coding Conventions
|
||||
|
||||
### 1.1 Language Standards
|
||||
|
||||
- **Primary Language:** Kotlin (JVM/Multiplatform)
|
||||
- **Java Compatibility:** Target Java 21+
|
||||
- **Kotlin Version:** Latest stable version
|
||||
- **Code Style:** Official Kotlin coding conventions
|
||||
|
||||
### 1.2 Naming Conventions
|
||||
|
||||
#### Classes and Interfaces
|
||||
```kotlin
|
||||
// Use PascalCase for classes and interfaces
|
||||
class MemberService
|
||||
interface EventRepository
|
||||
data class MemberRegistration
|
||||
sealed class AuthResult
|
||||
|
||||
// Use descriptive names that reflect domain concepts
|
||||
class HorseRegistrationService // Good
|
||||
class HRS // Avoid abbreviations
|
||||
```
|
||||
|
||||
#### Functions and Variables
|
||||
```kotlin
|
||||
// Use camelCase for functions and variables
|
||||
fun authenticateUser(): AuthResult
|
||||
val memberRepository: MemberRepository
|
||||
suspend fun findByEmail(email: EmailAddress): Result<Member?, RepositoryError>
|
||||
|
||||
// Use descriptive test method names with "should" statements
|
||||
@Test
|
||||
fun `authenticate should return Success for valid credentials`()
|
||||
```
|
||||
|
||||
#### Constants and Enums
|
||||
```kotlin
|
||||
// Use SCREAMING_SNAKE_CASE for constants
|
||||
const val MAX_RETRY_ATTEMPTS = 3
|
||||
const val DEFAULT_TIMEOUT_MS = 5000L
|
||||
|
||||
// Use PascalCase for enum values
|
||||
enum class MemberStatus {
|
||||
ACTIVE,
|
||||
INACTIVE,
|
||||
SUSPENDED
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Code Structure Principles
|
||||
|
||||
#### Result Pattern Usage
|
||||
```kotlin
|
||||
// Always use Result pattern for operations that can fail
|
||||
interface MemberRepository {
|
||||
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
|
||||
suspend fun save(member: Member): Result<Unit, RepositoryError>
|
||||
}
|
||||
|
||||
// Result extensions for error handling
|
||||
inline fun <T, E, R> Result<T, E>.mapError(transform: (E) -> R): Result<T, R> =
|
||||
when (this) {
|
||||
is Result.Success -> Result.Success(value)
|
||||
is Result.Failure -> Result.Failure(transform(error))
|
||||
}
|
||||
```
|
||||
|
||||
#### Coroutines and Async Programming
|
||||
```kotlin
|
||||
// Use suspend functions for async operations
|
||||
suspend fun processEventBatch(events: List<DomainEvent>): Result<Unit, ProcessingError>
|
||||
|
||||
// Prefer structured concurrency
|
||||
class EventProcessor {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
suspend fun processEvents() = withContext(scope.coroutineContext) {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Documentation Standards
|
||||
```kotlin
|
||||
/**
|
||||
* Authenticates a user with the given credentials.
|
||||
*
|
||||
* @param credentials The user credentials containing username and password
|
||||
* @return AuthResult.Success with user data if authentication succeeds,
|
||||
* AuthResult.Failure with error details if it fails
|
||||
*/
|
||||
suspend fun authenticate(credentials: UserCredentials): AuthResult
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Code Organization and Package Structure
|
||||
|
||||
### 2.1 Overall Architecture
|
||||
|
||||
The project follows a **microservices architecture** with **Domain-Driven Design (DDD)** principles and **Clean Architecture** patterns.
|
||||
|
||||
#### High-Level Structure
|
||||
```
|
||||
Meldestelle/
|
||||
├── core/ # Shared kernel - fundamental building blocks
|
||||
│ ├── core-domain/ # Common domain types and interfaces
|
||||
│ └── core-utils/ # Shared utilities and extensions
|
||||
├── infrastructure/ # Cross-cutting infrastructure services
|
||||
│ ├── auth/ # Authentication & authorization
|
||||
│ ├── messaging/ # Event messaging (Kafka)
|
||||
│ ├── cache/ # Distributed caching (Redis)
|
||||
│ ├── gateway/ # API Gateway
|
||||
│ └── monitoring/ # Observability and monitoring
|
||||
├── [domain-services]/ # Domain-specific microservices
|
||||
│ ├── members/ # Member management
|
||||
│ ├── events/ # Event management
|
||||
│ ├── horses/ # Horse registry
|
||||
│ └── masterdata/ # Master data management
|
||||
├── client/ # Client applications
|
||||
│ ├── common-ui/ # Shared UI components (KMP)
|
||||
│ ├── desktop-app/ # Desktop application
|
||||
│ └── web-app/ # Web application
|
||||
└── platform/ # Build and dependency management
|
||||
```
|
||||
|
||||
### 2.2 Microservice Structure (Clean Architecture)
|
||||
|
||||
Each domain service follows a **4-layer architecture**:
|
||||
|
||||
```
|
||||
domain-service/
|
||||
├── domain-api/ # REST controllers, DTOs, API contracts
|
||||
├── domain-application/ # Use cases, application logic, orchestration
|
||||
├── domain-domain/ # Domain models, business rules, interfaces
|
||||
└── domain-infrastructure/ # Technical implementations (DB, external APIs)
|
||||
```
|
||||
|
||||
#### Layer Responsibilities
|
||||
|
||||
**`:domain-api` Layer:**
|
||||
```kotlin
|
||||
// REST Controllers
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/members")
|
||||
class MemberController(private val memberService: MemberService)
|
||||
|
||||
// DTOs for external communication
|
||||
data class MemberRegistrationRequest(
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String
|
||||
)
|
||||
```
|
||||
|
||||
**`:domain-application` Layer:**
|
||||
```kotlin
|
||||
// Use cases and application services
|
||||
class MemberApplicationService(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val eventPublisher: EventPublisher
|
||||
) {
|
||||
suspend fun registerMember(command: RegisterMemberCommand): Result<MemberId, MemberError>
|
||||
}
|
||||
```
|
||||
|
||||
**`:domain-domain` Layer:**
|
||||
```kotlin
|
||||
// Domain models and business logic
|
||||
data class Member(
|
||||
val id: MemberId,
|
||||
val personalInfo: PersonalInfo,
|
||||
val membershipStatus: MembershipStatus
|
||||
) {
|
||||
fun activate(): Member = copy(membershipStatus = MembershipStatus.ACTIVE)
|
||||
}
|
||||
|
||||
// Repository interfaces (implemented in infrastructure)
|
||||
interface MemberRepository {
|
||||
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
|
||||
suspend fun save(member: Member): Result<Unit, RepositoryError>
|
||||
}
|
||||
```
|
||||
|
||||
**`:domain-infrastructure` Layer:**
|
||||
```kotlin
|
||||
// Technical implementations
|
||||
class ExposedMemberRepository(
|
||||
private val database: Database
|
||||
) : MemberRepository {
|
||||
override suspend fun findById(id: MemberId): Result<Member?, RepositoryError> {
|
||||
// Database implementation using Exposed ORM
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Package Naming Conventions
|
||||
|
||||
```kotlin
|
||||
// Base package structure
|
||||
at.mocode.[layer].[domain].[component]
|
||||
|
||||
// Examples
|
||||
at.mocode.members.domain.model // Domain models
|
||||
at.mocode.members.application.service // Application services
|
||||
at.mocode.members.infrastructure.persistence // Persistence layer
|
||||
at.mocode.infrastructure.messaging.kafka // Infrastructure components
|
||||
at.mocode.core.utils.result // Core utilities
|
||||
```
|
||||
|
||||
### 2.4 Dependency Rules
|
||||
|
||||
- **Core modules** must not depend on any other modules
|
||||
- **Domain layer** must not depend on infrastructure or application layers
|
||||
- **Application layer** can depend on domain layer only
|
||||
- **Infrastructure layer** can depend on domain and application layers
|
||||
- **API layer** orchestrates calls between application and infrastructure
|
||||
|
||||
---
|
||||
|
||||
## 3. Unit and Integration Testing Approaches
|
||||
|
||||
### 3.1 Testing Strategy Overview
|
||||
|
||||
The project follows a **comprehensive testing strategy** with multiple testing levels:
|
||||
|
||||
1. **Unit Tests** - Fast, isolated tests for individual components
|
||||
2. **Integration Tests** - Tests for component interactions
|
||||
3. **Performance Tests** - Load and throughput testing
|
||||
4. **End-to-End Tests** - Full system workflow testing
|
||||
|
||||
### 3.2 Testing Stack
|
||||
|
||||
#### Core Testing Libraries
|
||||
```kotlin
|
||||
// Unit testing
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
|
||||
testImplementation("io.mockk:mockk:1.13.8")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
|
||||
// Integration testing
|
||||
testImplementation("org.testcontainers:junit-jupiter:1.19.1")
|
||||
testImplementation("org.testcontainers:kafka:1.19.1")
|
||||
testImplementation("org.testcontainers:postgresql:1.19.1")
|
||||
|
||||
// Performance testing
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
```
|
||||
|
||||
### 3.3 Unit Testing Conventions
|
||||
|
||||
#### Test Structure and Naming
|
||||
```kotlin
|
||||
class AuthenticationServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
// Test setup
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate should return Success for valid credentials`() = runTest {
|
||||
// Given
|
||||
val credentials = UserCredentials("user@example.com", "validPassword")
|
||||
coEvery { userRepository.findByEmail(any()) } returns Result.Success(testUser)
|
||||
|
||||
// When
|
||||
val result = authenticationService.authenticate(credentials)
|
||||
|
||||
// Then
|
||||
assertTrue(result is AuthResult.Success)
|
||||
assertEquals(testUser.id, result.user.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate should return Failure for invalid credentials`() = runTest {
|
||||
// Given - When - Then pattern
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mocking Best Practices
|
||||
```kotlin
|
||||
class MemberServiceTest {
|
||||
private val memberRepository = mockk<MemberRepository>()
|
||||
private val eventPublisher = mockk<EventPublisher>()
|
||||
private val memberService = MemberService(memberRepository, eventPublisher)
|
||||
|
||||
@Test
|
||||
fun `should publish event when member is registered`() = runTest {
|
||||
// Mock repository responses
|
||||
coEvery { memberRepository.save(any()) } returns Result.Success(Unit)
|
||||
coEvery { eventPublisher.publish(any()) } returns Result.Success(Unit)
|
||||
|
||||
// Test implementation
|
||||
val result = memberService.registerMember(validCommand)
|
||||
|
||||
// Verify interactions
|
||||
coVerify { eventPublisher.publish(any<MemberRegisteredEvent>()) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Integration Testing Approaches
|
||||
|
||||
#### Database Integration Tests
|
||||
```kotlin
|
||||
@Testcontainers
|
||||
class MemberRepositoryIntegrationTest {
|
||||
|
||||
companion object {
|
||||
@Container
|
||||
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should persist and retrieve member correctly`() = runTest {
|
||||
// Test with real database using Testcontainers
|
||||
val member = createTestMember()
|
||||
|
||||
val saveResult = memberRepository.save(member)
|
||||
assertTrue(saveResult.isSuccess())
|
||||
|
||||
val retrievedResult = memberRepository.findById(member.id)
|
||||
assertTrue(retrievedResult.isSuccess())
|
||||
assertEquals(member, retrievedResult.getOrNull())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Messaging Integration Tests
|
||||
```kotlin
|
||||
@Testcontainers
|
||||
class KafkaEventPublisherIntegrationTest {
|
||||
|
||||
companion object {
|
||||
@Container
|
||||
val kafka = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should publish and consume events correctly`() = runTest {
|
||||
val event = MemberRegisteredEvent(memberId = MemberId.generate())
|
||||
|
||||
val publishResult = eventPublisher.publish(event)
|
||||
assertTrue(publishResult.isSuccess())
|
||||
|
||||
// Verify event was consumed
|
||||
val consumedEvents = eventConsumer.consumeEvents(timeout = 5.seconds)
|
||||
assertTrue(consumedEvents.any { it.memberId == event.memberId })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Performance Testing
|
||||
|
||||
#### Batch Processing Performance Tests
|
||||
```kotlin
|
||||
class KafkaBatchPerformanceTest {
|
||||
|
||||
@Test
|
||||
fun `should process large batches within acceptable time limits`() = runTest {
|
||||
val batchSize = 1000
|
||||
val events = generateTestEvents(batchSize)
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
val results = eventProcessor.processBatch(events)
|
||||
val processingTime = System.currentTimeMillis() - startTime
|
||||
|
||||
assertTrue(results.all { it.isSuccess() })
|
||||
assertTrue(processingTime < 5000) // Should complete within 5 seconds
|
||||
|
||||
println("[DEBUG_LOG] Processed $batchSize events in ${processingTime}ms")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 Test Organization
|
||||
|
||||
#### Directory Structure
|
||||
```
|
||||
src/
|
||||
├── main/kotlin/ # Production code
|
||||
└── test/kotlin/ # Test code
|
||||
├── unit/ # Unit tests (optional sub-organization)
|
||||
├── integration/ # Integration tests
|
||||
└── performance/ # Performance tests
|
||||
```
|
||||
|
||||
#### Test Categories and Execution
|
||||
```kotlin
|
||||
// Use JUnit 5 tags for test categorization
|
||||
@Tag("unit")
|
||||
class MemberServiceTest
|
||||
|
||||
@Tag("integration")
|
||||
class MemberRepositoryIntegrationTest
|
||||
|
||||
@Tag("performance")
|
||||
class KafkaBatchPerformanceTest
|
||||
```
|
||||
|
||||
### 3.7 Testing Guidelines
|
||||
|
||||
#### Best Practices
|
||||
1. **Test Method Naming:** Use descriptive names with "should" statements
|
||||
2. **AAA Pattern:** Arrange, Act, Assert structure
|
||||
3. **One Assertion Per Test:** Focus on single behavior
|
||||
4. **Test Data Builders:** Use factory methods for test data creation
|
||||
5. **Coroutine Testing:** Use `runTest` for suspend functions
|
||||
6. **Mock Verification:** Verify important interactions, not implementation details
|
||||
|
||||
#### Coverage Goals
|
||||
- **Unit Tests:** 80%+ code coverage for domain and application layers
|
||||
- **Integration Tests:** Cover all repository implementations and external integrations
|
||||
- **Performance Tests:** Cover critical batch operations and high-load scenarios
|
||||
|
||||
#### Debugging Support
|
||||
```kotlin
|
||||
// Always prefix debug messages with [DEBUG_LOG]
|
||||
@Test
|
||||
fun `should handle concurrent requests`() = runTest {
|
||||
println("[DEBUG_LOG] Starting concurrent request test with ${requestCount} requests")
|
||||
|
||||
// Test implementation
|
||||
|
||||
println("[DEBUG_LOG] Completed test. Success rate: ${successCount}/${requestCount}")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Additional Development Standards
|
||||
|
||||
### 4.1 Error Handling
|
||||
- Use `Result` pattern consistently for operations that can fail
|
||||
- Define domain-specific error types
|
||||
- Avoid throwing exceptions in domain logic
|
||||
|
||||
### 4.2 Logging and Monitoring
|
||||
- Use structured logging with appropriate log levels
|
||||
- Include correlation IDs for request tracing
|
||||
- Monitor key business metrics and technical performance
|
||||
|
||||
### 4.3 Security Considerations
|
||||
- Validate all external inputs
|
||||
- Use JWT tokens for authentication
|
||||
- Implement proper authorization checks
|
||||
- Secure sensitive configuration data
|
||||
|
||||
---
|
||||
|
||||
This guideline is a living document and should be updated as the project evolves and new patterns emerge.
|
||||
@@ -0,0 +1,161 @@
|
||||
# Meldestelle_Pro: Entwicklungs-Guideline
|
||||
|
||||
**Status:** Finalisiert & Verbindlich
|
||||
**Version:** 1.0
|
||||
**Stand:** 15. August 2025
|
||||
|
||||
## 1. 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.
|
||||
|
||||
Unsere Architektur basiert auf **vier Säulen**:
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 2. Coding Conventions & Code-Qualität
|
||||
|
||||
### 2.1. 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.
|
||||
|
||||
### 2.2. 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`)
|
||||
|
||||
### 2.3. 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 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4. 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")
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend-Entwicklungsrichtlinien
|
||||
|
||||
### 3.1. Microservice-Struktur (Clean Architecture)
|
||||
|
||||
Jeder fachliche Microservice folgt der 4-Layer-Struktur (`api`, `application`, `domain`, `infrastructure`).
|
||||
|
||||
### 3.2. 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>
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3. Messaging & Event-Naming
|
||||
|
||||
* **Event-Naming Convention:** Domänen-Events folgen dem Muster `{Domain}{Entity}{Action}Event`.
|
||||
|
||||
```kotlin
|
||||
data class MemberPersonalDataUpdatedEvent(...) : DomainEvent(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 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.
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing
|
||||
|
||||
Tests sind ein integraler Bestandteil jedes Features und müssen einen hohen Standard erfüllen.
|
||||
|
||||
### 5.1. Test-Pyramide & Werkzeuge
|
||||
|
||||
* **Unit-Tests (80 %+ Abdeckung):** Für Domänen- und Anwendungslogik (JUnit 5, MockK).
|
||||
|
||||
* **Integrationstests:** Decken alle Repository-Implementierungen und externen Integrationen ab.
|
||||
|
||||
* **Testcontainers als Goldstandard:** Jede Interaktion mit externer Infrastruktur (DB, Cache, Broker) **muss** mit
|
||||
**Testcontainers** getestet werden.
|
||||
|
||||
### 5.2. Debugging
|
||||
|
||||
Debug-Ausgaben im Test-Code müssen mit `[DEBUG_LOG]` beginnen, um sie leicht identifizieren und filtern zu können.
|
||||
|
||||
---
|
||||
|
||||
## 6. Infrastruktur- & Betriebs-Spezifikationen
|
||||
|
||||
### 6.1. Kafka-Konfiguration
|
||||
|
||||
Die Konfiguration muss auf maximale Zuverlässigkeit ausgelegt sein:
|
||||
|
||||
```yaml
|
||||
# in 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
|
||||
```
|
||||
|
||||
### 6.2. 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`)
|
||||
|
||||
### 6.3. 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.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Guideline: Zyklus "Tracer Bullet"
|
||||
|
||||
* **Zyklus-Start:** 15. August 2025
|
||||
* **Status:** In Arbeit
|
||||
* **Basis:** Diese Guideline erweitert die [Master-Guideline](./master-guideline.md)
|
||||
|
||||
## 1. Ziel des Zyklus
|
||||
|
||||
Das oberste und einzige Ziel dieses Entwicklungszyklus ist die **Validierung der gesamten technischen Architektur
|
||||
End-to-End**. Wir wollen beweisen, dass eine Anfrage vom Client den gesamten technischen Stack (Gateway, Service
|
||||
Discovery, Backend-Service) erfolgreich durchlaufen und eine Antwort zurückliefern kann.
|
||||
|
||||
Am Ende dieses Zyklus werden wir einen stabilen, qualitätsgesicherten und dokumentierten Unterbau haben, auf dem die
|
||||
Entwicklung der fachlichen Features aufsetzen kann.
|
||||
|
||||
## 2. Umfang (Was gehört zu diesem Zyklus?)
|
||||
|
||||
Die folgenden Module und Aufgaben sind Teil dieses Zyklus:
|
||||
|
||||
* **Backend-Infrastruktur (`:core` & `:infrastructure`):**
|
||||
* Vollständige Überarbeitung, Optimierung und Testabdeckung aller Infrastruktur-Module (`cache`, `event-store`,
|
||||
`auth`, `messaging`, `monitoring`, `gateway`).
|
||||
* Implementierung einer robusten Logging- und Konfigurations-Infrastruktur.
|
||||
* **Temporärer Test-Service (`:temp:ping-service`):**
|
||||
* Erstellung eines minimalen Spring-Boot-Service, der nur einen `GET /ping`-Endpunkt bereitstellt.
|
||||
* **Frontend-Infrastruktur (`:client`):**
|
||||
* Aufbau einer sauberen, leeren Grundstruktur für die Kotlin Multiplatform App nach dem MVVM-Muster.
|
||||
* Implementierung einer minimalen UI mit einem "Ping"-Button und einem Anzeigefeld für die Antwort.
|
||||
|
||||
## 3. Spezifische Richtlinien für diesen Zyklus
|
||||
|
||||
* **Fokus auf Technik, nicht Fachlichkeit:** Jede Zeile Code, die in diesem Zyklus geschrieben wird, dient
|
||||
ausschließlich der Stabilisierung der technischen Infrastruktur. Es wird keine komplexe Geschäftslogik implementiert.
|
||||
* **Qualitätsstandards gelten uneingeschränkt:** Auch für diesen technischen Zyklus gelten alle Regeln der
|
||||
Master-Guideline. Insbesondere:
|
||||
* **Tests sind Pflicht:** Jede neue oder geänderte Komponente muss durch Tests (insbesondere **Testcontainers** für
|
||||
Infrastruktur) abgesichert werden.
|
||||
* **Kein `println`:** Es wird ausschließlich der strukturierte Logger verwendet.
|
||||
* **Dokumentation ist Teil der Aufgabe:** Jedes Modul, das wir überarbeiten, wird mit einer aktualisierten und präzisen
|
||||
`README.md`-Datei abgeschlossen.
|
||||
|
||||
## 4. Definition of Done (Wann sind wir fertig?)
|
||||
|
||||
Dieser Zyklus ist abgeschlossen, wenn **alle** der folgenden Kriterien erfüllt sind:
|
||||
|
||||
* [ ] Alle `:core` und `:infrastructure`-Module wurden überarbeitet, sind fehlerfrei testbar und ihre `README.md`
|
||||
-Dateien sind auf dem neuesten Stand.
|
||||
* [ ] Der `:temp:ping-service` ist implementiert, getestet und lauffähig.
|
||||
* [ ] Die `:client:web-app` ist mit einer sauberen MVVM-Struktur aufgesetzt und startet fehlerfrei.
|
||||
* [ ] **Der End-to-End "Tracer Bullet"-Test ist erfolgreich:**
|
||||
* [ ] Alle Docker-Container (`docker-compose up`) starten.
|
||||
* [ ] Der `gateway`-Service startet.
|
||||
* [ ] Der `ping-service` startet und registriert sich erfolgreich bei Consul.
|
||||
* [ ] Die `web-app` startet.
|
||||
* [ ] Ein Klick auf den "Ping"-Button in der Web-App führt zu einer `GET`-Anfrage an das Gateway, wird korrekt an
|
||||
den `ping-service` weitergeleitet und die Antwort `"pong"` wird erfolgreich in der UI angezeigt.
|
||||
* [ ] Der gesamte `clean build` des Projekts läuft ohne Fehler und **ohne Warnungen**.
|
||||
* [ ] Die `master-guideline.md` und die `trace-bullet-guideline.md` sind finalisiert.
|
||||
|
||||
## 5. Lessons Learned (nach Abschluss)
|
||||
|
||||
- [ ] Was hat gut funktioniert?
|
||||
- [ ] Was würden wir beim nächsten Zyklus anders machen?
|
||||
- [ ] Welche Standards müssen in die Master-Guideline übernommen werden?
|
||||
-711
@@ -1,711 +0,0 @@
|
||||
# Meldestelle_Pro: Entwicklungs-Guideline
|
||||
|
||||
**Status:** Finalisiert & Verbindlich
|
||||
**Version:** 2.0
|
||||
**Stand:** August 2025
|
||||
|
||||
## 1. 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.
|
||||
|
||||
Unsere Architektur basiert auf **vier Säulen**:
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend-Entwicklungsrichtlinien
|
||||
|
||||
#### 2.1. Microservice-Struktur (Clean Architecture)
|
||||
|
||||
**Jeder fachliche Microservice (z.B. :members, :events) muss der etablierten 4-Layer-Struktur folgen:**
|
||||
|
||||
* **`:*-api`**: Definiert die öffentliche Schnittstelle des Service (REST-Controller, DTOs).
|
||||
* **`:*-application`**: Enthält die Anwendungslogik und Use Cases. Hier werden die Repositories orchestriert.
|
||||
* **`:*-domain`**: Das Herz des Service. Enthält die reinen, von Frameworks unabhängigen Domänenmodelle, Geschäftsregeln
|
||||
und Repository-Interfaces.
|
||||
* **`:*-infrastructure`**: Die technische Implementierung der Interfaces aus der Domänenschicht (z.B. Datenbankzugriff
|
||||
mit Exposed).
|
||||
|
||||
#### 2.2. Domain-Driven Design (DDD) in der Praxis
|
||||
|
||||
* **Shared Kernel (`:core`-Modul):** Das `:core`-Modul ist heilig. Es darf **ausschließlich** fundamentalen,
|
||||
domänen-agnostischen Code enthalten. Fachspezifische Konzepte gehören in ihre jeweilige Domäne.
|
||||
* **Repository-Pattern mit `Result`:** Jede Repository-Methode muss das `Result`-Pattern verwenden, um Erfolgs- und
|
||||
Fehlerfälle explizit und typsicher zu behandeln.
|
||||
|
||||
```kotlin
|
||||
// Repository mit Result-Pattern
|
||||
interface MemberRepository {
|
||||
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
|
||||
suspend fun save(member: Member): Result<Unit, RepositoryError>
|
||||
suspend fun findByEmail(email: EmailAddress): Result<List<Member>, RepositoryError>
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3. Core-Modul Spezifikation
|
||||
|
||||
Das `:core`-Modul definiert die fundamentalen Bausteine der gesamten Anwendung:
|
||||
|
||||
* **Result Extensions:** Utility-Funktionen für typsichere Fehlerbehandlung
|
||||
* **Common Types:** Basistypen für alle Domänen
|
||||
* **Shared Utilities:** Plattformunabhängige Hilfsfunktionen
|
||||
|
||||
```kotlin
|
||||
// Result Extensions im core-utils Modul
|
||||
inline fun <T, E, R> Result<T, E>.mapError(transform: (E) -> R): Result<T, R> =
|
||||
when (this) {
|
||||
is Result.Success -> Result.Success(value)
|
||||
is Result.Failure -> Result.Failure(transform(error))
|
||||
}
|
||||
|
||||
inline fun <T, E> Result<T, E>.onFailure(action: (E) -> Unit): Result<T, E> =
|
||||
also { if (it is Result.Failure) action(it.error) }
|
||||
|
||||
// Common Domain Types
|
||||
@JvmInline
|
||||
value class CorrelationId(val value: UUID) {
|
||||
companion object {
|
||||
fun generate(): CorrelationId = CorrelationId(UUID.randomUUID())
|
||||
fun of(value: String): Result<CorrelationId, ValidationError> =
|
||||
runCatching { UUID.fromString(value) }
|
||||
.map { CorrelationId(it) }
|
||||
.mapError { ValidationError.InvalidUUID("Invalid correlation ID: $value") }
|
||||
}
|
||||
}
|
||||
|
||||
// Konkrete Error-Implementierungen
|
||||
sealed class ValidationError(code: String, message: String) : DomainError(code, message) {
|
||||
data class InvalidUUID(override val message: String) :
|
||||
ValidationError("INVALID_UUID", message)
|
||||
data class InvalidEmail(override val message: String) :
|
||||
ValidationError("INVALID_EMAIL", message)
|
||||
data class InvalidLength(val field: String, val min: Int, val max: Int) :
|
||||
ValidationError("INVALID_LENGTH", "Field $field must be between $min and $max characters")
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4. Messaging & Event-Naming
|
||||
|
||||
* **Asynchrone Kommunikation:** Die bevorzugte Kommunikationsmethode ist asynchron über Kafka.
|
||||
* **Event-Naming Convention:** Domänen-Events folgen dem Muster `{Domain}{Entity}{Action}Event`.
|
||||
|
||||
```kotlin
|
||||
// Event-Naming Convention
|
||||
sealed class DomainEvent(
|
||||
val aggregateId: String,
|
||||
val version: Long,
|
||||
val timestamp: Instant = Instant.now()
|
||||
) {
|
||||
// Pattern: {Domain}{Entity}{Action}Event
|
||||
data class MemberPersonalDataUpdatedEvent(
|
||||
val memberId: MemberId,
|
||||
val personalData: PersonalData
|
||||
) : DomainEvent(memberId.value, version)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend-Entwicklungsrichtlinien
|
||||
|
||||
#### 3.1. Architekturmuster: MVVM & KMP
|
||||
|
||||
Das Frontend folgt konsequent dem **Model-View-ViewModel (MVVM)**-Muster und der **Kotlin Multiplatform (KMP)**-Strategie:
|
||||
|
||||
* **Model & ViewModel:** Die gesamte Geschäftslogik, der Zustand und die API-Aufrufe leben im `:client:common-ui`-Modul und sind plattformunabhängig.
|
||||
* **View:** Die Benutzeroberfläche wird mit **Compose Multiplatform** im `:client:common-ui`-Modul implementiert.
|
||||
|
||||
#### 3.2. State Management
|
||||
|
||||
**Unidirectional Data Flow mit MVI-Pattern:**
|
||||
|
||||
```kotlin
|
||||
// State Management Pattern
|
||||
@Stable
|
||||
data class MemberListUiState(
|
||||
val members: List<Member> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val searchQuery: String = ""
|
||||
)
|
||||
|
||||
sealed class MemberListIntent {
|
||||
object LoadMembers : MemberListIntent()
|
||||
data class SearchMembers(val query: String) : MemberListIntent()
|
||||
data class DeleteMember(val memberId: MemberId) : MemberListIntent()
|
||||
}
|
||||
|
||||
class MemberListViewModel(
|
||||
private val memberRepository: MemberRepository
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(MemberListUiState())
|
||||
val uiState: StateFlow<MemberListUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun handleIntent(intent: MemberListIntent) {
|
||||
when (intent) {
|
||||
is MemberListIntent.LoadMembers -> loadMembers()
|
||||
is MemberListIntent.SearchMembers -> searchMembers(intent.query)
|
||||
is MemberListIntent.DeleteMember -> deleteMember(intent.memberId)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3. Navigation Architecture
|
||||
|
||||
**Compose Navigation mit typsicheren Routes:**
|
||||
|
||||
```kotlin
|
||||
// Navigation Definition
|
||||
@Serializable
|
||||
sealed class Screen {
|
||||
@Serializable
|
||||
object MemberList : Screen()
|
||||
|
||||
@Serializable
|
||||
data class MemberDetail(val memberId: String) : Screen()
|
||||
|
||||
@Serializable
|
||||
data class EventRegistration(val eventId: String, val memberId: String) : Screen()
|
||||
}
|
||||
|
||||
// Navigation Router
|
||||
class NavigationRouter {
|
||||
private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
|
||||
val navigationEvents: SharedFlow<NavigationEvent> = _navigationEvents.asSharedFlow()
|
||||
|
||||
fun navigateTo(screen: Screen) {
|
||||
_navigationEvents.tryEmit(NavigationEvent.NavigateTo(screen))
|
||||
}
|
||||
|
||||
fun navigateBack() {
|
||||
_navigationEvents.tryEmit(NavigationEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4. Vertikale Schnitte (Features)
|
||||
|
||||
Der UI-Code wird nach **fachlichen Features** strukturiert. Ein Feature (z.B. "Nennungsabwicklung") hat sein eigenes Verzeichnis und enthält alle zugehörigen Views, ViewModels und Models:
|
||||
|
||||
```
|
||||
client/common-ui/src/commonMain/kotlin/
|
||||
├── features/
|
||||
│ ├── members/
|
||||
│ │ ├── presentation/
|
||||
│ │ │ ├── MemberListViewModel.kt
|
||||
│ │ │ ├── MemberDetailViewModel.kt
|
||||
│ │ │ └── MemberUiState.kt
|
||||
│ │ ├── ui/
|
||||
│ │ │ ├── MemberListScreen.kt
|
||||
│ │ │ ├── MemberDetailScreen.kt
|
||||
│ │ │ └── components/
|
||||
│ │ └── domain/
|
||||
│ │ └── MemberUseCases.kt
|
||||
│ └── events/
|
||||
│ ├── presentation/
|
||||
│ ├── ui/
|
||||
│ └── domain/
|
||||
```
|
||||
|
||||
#### 3.5. Platform-spezifische Implementierungen
|
||||
|
||||
**Desktop-spezifische Features:**
|
||||
|
||||
```kotlin
|
||||
// Desktop-specific implementations
|
||||
actual class PlatformFileManager {
|
||||
actual suspend fun selectFile(): Result<File?, FileError> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val fileChooser = JFileChooser()
|
||||
val result = fileChooser.showOpenDialog(null)
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
Result.Success(fileChooser.selectedFile)
|
||||
} else {
|
||||
Result.Success(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Failure(FileError.SelectionFailed(e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Web-specific implementations
|
||||
actual class PlatformFileManager {
|
||||
actual suspend fun selectFile(): Result<File?, FileError> {
|
||||
return try {
|
||||
val input = document.createElement("input") as HTMLInputElement
|
||||
input.type = "file"
|
||||
input.click()
|
||||
// Implementation für Web File API
|
||||
Result.Success(null) // Simplified
|
||||
} catch (e: Exception) {
|
||||
Result.Failure(FileError.SelectionFailed(e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API-Versioning & Kompatibilität
|
||||
|
||||
#### 4.1. Versioning-Strategie
|
||||
|
||||
**Header-basierte Versionierung (Empfohlen):**
|
||||
|
||||
```kotlin
|
||||
// API Version Header
|
||||
@RestController
|
||||
@RequestMapping("/api/members")
|
||||
class MemberController {
|
||||
|
||||
@GetMapping
|
||||
fun getMembers(
|
||||
@RequestHeader(value = "API-Version", defaultValue = "1.0") version: String,
|
||||
@RequestParam query: String?
|
||||
): ResponseEntity<List<MemberDto>> {
|
||||
return when (version) {
|
||||
"1.0" -> memberService.getMembersV1(query)
|
||||
"2.0" -> memberService.getMembersV2(query)
|
||||
else -> ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Client-seitige Versionierung
|
||||
class ApiClient {
|
||||
companion object {
|
||||
const val CURRENT_API_VERSION = "2.0"
|
||||
const val MIN_SUPPORTED_VERSION = "1.0"
|
||||
}
|
||||
|
||||
private val defaultHeaders = mapOf(
|
||||
"API-Version" to CURRENT_API_VERSION,
|
||||
"Accept" to "application/json"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2. Backward Compatibility Rules
|
||||
|
||||
* **Breaking Changes:** Erfordern eine neue Major-Version (1.x → 2.x)
|
||||
* **Additive Changes:** Können in Minor-Versionen erfolgen (1.0 → 1.1)
|
||||
* **Bug Fixes:** Patch-Versionen (1.0.0 → 1.0.1)
|
||||
|
||||
```kotlin
|
||||
// Compatibility Matrix
|
||||
object ApiCompatibility {
|
||||
val supportedVersions = mapOf(
|
||||
"2.0" to ApiVersionConfig(
|
||||
deprecated = false,
|
||||
sunsetDate = null,
|
||||
features = setOf("advanced-search", "bulk-operations")
|
||||
),
|
||||
"1.0" to ApiVersionConfig(
|
||||
deprecated = true,
|
||||
sunsetDate = LocalDate.of(2025, 12, 31),
|
||||
features = setOf("basic-search")
|
||||
)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3. Versioning Lifecycle Management
|
||||
|
||||
* **Deprecation Notice:** Mindestens 6 Monate vor Entfernung
|
||||
* **Documentation:** Alle Versionen müssen in OpenAPI dokumentiert sein
|
||||
* **Migration Guide:** Für jede Major-Version erforderlich
|
||||
|
||||
---
|
||||
|
||||
## 5. Allgemeine Qualitätsstandards
|
||||
|
||||
#### 4.1. Code-Qualität & Kotlin-Konventionen
|
||||
|
||||
* **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, um Fehler zu vermeiden.
|
||||
|
||||
```kotlin
|
||||
// Ergänzung für Value Objects
|
||||
@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 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2. Error-Handling
|
||||
|
||||
* **`Result`-Pattern statt Exceptions:** Für erwartbare Geschäftsfehler ist das `Result`-Pattern zu verwenden.
|
||||
* **Spezifische Fehler-Hierarchie:** Wir verwenden eine `sealed class`-Hierarchie, um Fehlerarten klar zu
|
||||
kategorisieren.
|
||||
|
||||
```kotlin
|
||||
// Spezifische Error-Hierarchie definieren
|
||||
sealed class DomainError(val code: String, val message: String)
|
||||
sealed class ValidationError(code: String, message: String) : DomainError(code, message)
|
||||
sealed class BusinessError(code: String, message: String) : DomainError(code, message)
|
||||
sealed class TechnicalError(code: String, message: String) : DomainError(code, message)
|
||||
```
|
||||
|
||||
#### 4.3. Testing
|
||||
|
||||
* **Testcontainers als Goldstandard:** Jede Interaktion mit externer Infrastruktur (DB, Cache, Broker) **muss** mit *
|
||||
*Testcontainers** getestet werden.
|
||||
* **Mocking für Isolation:** Abhängigkeiten innerhalb von Tests werden mit Mocking-Frameworks (z.B. MockK) isoliert, um
|
||||
den Testfokus zu schärfen.
|
||||
|
||||
```kotlin
|
||||
// Testcontainers-Pattern für Infrastruktur-Tests
|
||||
@TestConfiguration
|
||||
class KafkaTestConfig {
|
||||
@Bean
|
||||
@Primary
|
||||
fun kafkaEventPublisher(): KafkaEventPublisher = mockk()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Infrastruktur-Spezifikationen
|
||||
|
||||
#### 5.1. Kafka-Konfiguration
|
||||
|
||||
Die Konfiguration für Producer und Consumer muss produktionsreife Einstellungen für Zuverlässigkeit und Datenkonsistenz
|
||||
verwenden.
|
||||
|
||||
```YAML
|
||||
# Ergänzung für 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
|
||||
```
|
||||
|
||||
#### 5.2. Datenbank-Migrationen mit 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`)
|
||||
|
||||
---
|
||||
|
||||
## 6. Monitoring & Observability
|
||||
|
||||
#### 6.1. Structured Logging
|
||||
|
||||
Logs müssen als strukturierte Daten (z.B. JSON) ausgegeben werden und immer eine Korrelations-ID enthalten, um Anfragen über Service-Grenzen hinweg verfolgen zu können.
|
||||
|
||||
```kotlin
|
||||
// Korrigierte Logging-Syntax
|
||||
@Component
|
||||
class MemberService {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
suspend fun createMember(command: CreateMemberCommand) {
|
||||
logger.info {
|
||||
mapOf(
|
||||
"message" to "Creating member",
|
||||
"memberId" to command.memberId.value,
|
||||
"operation" to "create_member",
|
||||
"correlationId" to MDC.get("correlationId")
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2. Service Level Indicators (SLIs) & Objectives (SLOs)
|
||||
|
||||
**Definierte SLIs für alle Services:**
|
||||
|
||||
```kotlin
|
||||
// SLI/SLO Definitionen
|
||||
object ServiceLevelIndicators {
|
||||
|
||||
// Availability SLIs
|
||||
data class AvailabilitySLI(
|
||||
val serviceName: String,
|
||||
val targetUptime: Double = 0.995, // 99.5%
|
||||
val measurementWindow: Duration = Duration.ofDays(30)
|
||||
)
|
||||
|
||||
// Latency SLIs
|
||||
data class LatencySLI(
|
||||
val serviceName: String,
|
||||
val percentile: Double = 0.95, // P95
|
||||
val targetLatency: Duration = Duration.ofMillis(500),
|
||||
val measurementWindow: Duration = Duration.ofMinutes(5)
|
||||
)
|
||||
|
||||
// Error Rate SLIs
|
||||
data class ErrorRateSLI(
|
||||
val serviceName: String,
|
||||
val maxErrorRate: Double = 0.001, // 0.1%
|
||||
val measurementWindow: Duration = Duration.ofMinutes(5)
|
||||
)
|
||||
}
|
||||
|
||||
// SLO Monitoring
|
||||
@Component
|
||||
class SLOMonitor(private val meterRegistry: MeterRegistry) {
|
||||
|
||||
private val requestDuration = Timer.builder("http.request.duration")
|
||||
.description("HTTP request duration")
|
||||
.register(meterRegistry)
|
||||
|
||||
private val errorRate = Counter.builder("http.request.errors")
|
||||
.description("HTTP request errors")
|
||||
.register(meterRegistry)
|
||||
|
||||
fun recordRequest(duration: Duration, isError: Boolean) {
|
||||
requestDuration.record(duration)
|
||||
if (isError) errorRate.increment()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.3. Business & Technical Metrics
|
||||
|
||||
**Umfassende Metriken-Strategie:**
|
||||
|
||||
```kotlin
|
||||
// Business Metrics
|
||||
@Component
|
||||
class BusinessMetrics(meterRegistry: MeterRegistry) {
|
||||
|
||||
// Fachliche Metriken
|
||||
private val memberRegistrations = Counter.builder("business.member.registrations.total")
|
||||
.description("Total number of member registrations")
|
||||
.tag("service", "members")
|
||||
.register(meterRegistry)
|
||||
|
||||
private val eventParticipations = Counter.builder("business.event.participations.total")
|
||||
.description("Total event participations")
|
||||
.tag("service", "events")
|
||||
.register(meterRegistry)
|
||||
|
||||
private val paymentTransactions = Timer.builder("business.payment.transaction.duration")
|
||||
.description("Payment transaction processing time")
|
||||
.tag("service", "payments")
|
||||
.register(meterRegistry)
|
||||
|
||||
// Gauge für aktuelle Werte
|
||||
private val activeSessions = Gauge.builder("business.active.sessions")
|
||||
.description("Currently active user sessions")
|
||||
.register(meterRegistry) { getActiveSessionCount() }
|
||||
}
|
||||
|
||||
// Technical Metrics
|
||||
@Component
|
||||
class TechnicalMetrics(meterRegistry: MeterRegistry) {
|
||||
|
||||
// Database Metriken
|
||||
private val dbConnectionPool = Gauge.builder("database.connection.pool.active")
|
||||
.description("Active database connections")
|
||||
.register(meterRegistry) { getActiveConnections() }
|
||||
|
||||
// Kafka Metriken
|
||||
private val kafkaLag = Gauge.builder("kafka.consumer.lag")
|
||||
.description("Kafka consumer lag")
|
||||
.register(meterRegistry) { getConsumerLag() }
|
||||
|
||||
// Cache Metriken
|
||||
private val cacheHitRate = Gauge.builder("cache.hit.rate")
|
||||
.description("Cache hit rate percentage")
|
||||
.register(meterRegistry) { getCacheHitRate() }
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.4. Alerting Strategy
|
||||
|
||||
**Alert-Definitionen basierend auf SLOs:**
|
||||
|
||||
```yaml
|
||||
# Prometheus Alert Rules
|
||||
groups:
|
||||
- name: slo.rules
|
||||
rules:
|
||||
- alert: HighErrorRate
|
||||
expr: rate(http_request_errors_total[5m]) > 0.001
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "High error rate detected"
|
||||
|
||||
- alert: HighLatency
|
||||
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "High latency detected"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Zusätzliche Richtlinien
|
||||
|
||||
#### 7.1. Security
|
||||
|
||||
Die Autorisierung muss auf Methodenebene mit Spring Security Annotations (`@PreAuthorize`) durchgesetzt werden, um eine feingranulare Zugriffskontrolle zu gewährleisten.
|
||||
|
||||
**JWT Implementation:**
|
||||
|
||||
```kotlin
|
||||
// JWT Configuration
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun jwtAuthenticationFilter(): JwtAuthenticationFilter {
|
||||
return JwtAuthenticationFilter()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
return http
|
||||
.csrf { it.disable() }
|
||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||
.authorizeHttpRequests { auth ->
|
||||
auth.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/members/**").hasRole("USER")
|
||||
.requestMatchers(HttpMethod.POST, "/api/members/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
}
|
||||
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
// Method-level Security
|
||||
@RestController
|
||||
@RequestMapping("/api/members")
|
||||
class MemberController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasRole('USER') or @memberService.isOwner(#id, authentication.name)")
|
||||
fun getMember(@PathVariable id: String): MemberDto {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('ADMIN') or hasPermission(#memberDto, 'CREATE')")
|
||||
fun createMember(@RequestBody memberDto: MemberDto): MemberDto {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**OAuth2 Integration:**
|
||||
|
||||
```kotlin
|
||||
// OAuth2 Resource Server Configuration
|
||||
@Configuration
|
||||
class OAuth2Config {
|
||||
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
return NimbusJwtDecoder.withJwkSetUri("https://auth-provider/.well-known/jwks.json").build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
|
||||
val converter = JwtAuthenticationConverter()
|
||||
converter.setJwtGrantedAuthoritiesConverter { jwt ->
|
||||
val authorities = jwt.getClaimAsStringList("authorities") ?: emptyList()
|
||||
authorities.map { SimpleGrantedAuthority("ROLE_$it") }
|
||||
}
|
||||
return converter
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Permission Evaluator
|
||||
@Component("memberService")
|
||||
class MemberPermissionEvaluator {
|
||||
|
||||
fun isOwner(memberId: String, username: String): Boolean {
|
||||
return memberRepository.findById(memberId)
|
||||
?.let { it.email == username }
|
||||
?: false
|
||||
}
|
||||
|
||||
fun hasPermission(target: Any, permission: String): Boolean {
|
||||
// Custom permission logic
|
||||
return when (permission) {
|
||||
"CREATE" -> hasCreatePermission(target)
|
||||
"UPDATE" -> hasUpdatePermission(target)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rate Limiting:**
|
||||
|
||||
```kotlin
|
||||
// Rate Limiting Configuration
|
||||
@Configuration
|
||||
class RateLimitConfig {
|
||||
|
||||
@Bean
|
||||
fun rateLimitFilter(): RateLimitFilter {
|
||||
return RateLimitFilter(
|
||||
rateLimiters = mapOf(
|
||||
"/api/auth/login" to RateLimiter.create(5.0), // 5 requests per second
|
||||
"/api/members" to RateLimiter.create(100.0), // 100 requests per second
|
||||
"/api/events" to RateLimiter.create(50.0) // 50 requests per second
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Rate Limit Annotation
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class RateLimit(
|
||||
val requestsPerSecond: Double = 10.0,
|
||||
val burstCapacity: Int = 20
|
||||
)
|
||||
|
||||
// Usage
|
||||
@RestController
|
||||
class AuthController {
|
||||
|
||||
@PostMapping("/login")
|
||||
@RateLimit(requestsPerSecond = 5.0, burstCapacity = 10)
|
||||
fun login(@RequestBody loginRequest: LoginRequest): AuthResponse {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 7.2. Performance
|
||||
|
||||
Cache-Strategien (`@Cacheable`, `@CacheEvict`) **müssen gezielt eingesetzt werden**, um die Latenz bei häufigen Lesezugriffen zu minimieren.
|
||||
|
||||
#### 7.3. Dokumentation
|
||||
|
||||
Alle öffentlichen REST-Endpunkte müssen mit OpenAPI-Annotationen (`@Operation`, `@ApiResponse`) dokumentiert werden, um eine klare und interaktive API-Dokumentation zu generieren.
|
||||
Reference in New Issue
Block a user