22 KiB
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:
- Modularität & Skalierbarkeit durch eine Microservices-Architektur
- Fachlichkeit im Code durch Domain-Driven Design (DDD)
- Entkopplung & Resilienz durch eine ereignisgesteuerte Architektur (EDA)
- 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 dasResult-Pattern verwenden, um Erfolgs- und Fehlerfälle explizit und typsicher zu behandeln.// 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
// 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.// 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:
// 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:
// 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:
// 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):
// 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)
// 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.// 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 dasResult-Pattern zu verwenden. -
Spezifische Fehler-Hierarchie: Wir verwenden eine
sealed class-Hierarchie, um Fehlerarten klar zu kategorisieren.// 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.
// 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.
# 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.
// 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:
// 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:
// 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:
# 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:
// 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:
// 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:
// 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.