Files
meldestelle/.junie/guidelines/project-standards/architecture-principles.md
T
2025-09-15 12:49:55 +02:00

509 lines
16 KiB
Markdown

# 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<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