509 lines
16 KiB
Markdown
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
|