# Architecture Principles und Grundsätze --- guideline_type: "project-standards" scope: "architecture-principles" audience: ["developers", "architects", "ai-assistants"] last_updated: "2025-09-15" dependencies: ["master-guideline.md"] related_files: ["build.gradle.kts", "settings.gradle.kts", "docker-compose.yml"] ai_context: "Architektonische Grundlagen, Microservices-Pattern, DDD-Prinzipien, ereignisgesteuerte Architektur und Multiplatform-Strategie" --- ## 🏗️ 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 { 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 { // 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 { 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 { 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 { 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 suspend fun save(member: Member): Result suspend fun findByEmail(email: Email): Result suspend fun findByLicenseNumber(licenseNumber: LicenseNumber): Result suspend fun findAll(pageable: Pageable): Result, 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 = _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 = 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 = mutableListOf() ) { companion object { fun create( name: String, startDate: LocalDate, endDate: LocalDate, maxParticipants: Int ): Result { // 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 { // 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 ``` ## 🚀 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