- Detaillierter Plan zur Migration von alter zu neuer Modulstruktur - Umfasst Überführung von shared-kernel zu core-Modulen - Definiert Migration von Fachdomänen zu bounded contexts: * master-data → masterdata-Module * member-management → members-Module * horse-registry → horses-Module * event-management → events-Module - Beschreibt Verlagerung von api-gateway zu infrastructure/gateway - Strukturiert nach Domain-driven Design Prinzipien - Berücksichtigt Clean Architecture Layering (domain, application, infrastructure, api)
22 KiB
Core Module
Überblick
Das Core-Modul bildet das Fundament des gesamten Meldestelle-Systems und implementiert den Shared Kernel nach Domain-Driven Design Prinzipien. Es stellt gemeinsame Domänenkonzepte, Utilities und Infrastrukturkomponenten bereit, die von allen anderen Modulen (members, horses, events, masterdata, infrastructure) verwendet werden.
Architektur
Das Core-Modul ist in zwei Hauptkomponenten unterteilt:
core/
├── core-domain/ # Shared Domain Layer
│ ├── model/ # Gemeinsame Domain Models
│ │ ├── BaseDto.kt # Basis-DTO-Klassen
│ │ └── Enums.kt # Gemeinsame Enumerationen
│ ├── serialization/ # Serialisierung
│ │ └── Serializers.kt # Custom Serializers
│ └── event/ # Domain Events
│ └── DomainEvent.kt # Event-Infrastruktur
└── core-utils/ # Shared Utilities
├── config/ # Konfiguration
│ ├── AppConfig.kt # Anwendungskonfiguration
│ └── AppEnvironment.kt # Umgebungskonfiguration
├── database/ # Datenbank-Utilities
│ ├── DatabaseConfig.kt # Datenbank-Konfiguration
│ ├── DatabaseFactory.kt # Datenbank-Factory
│ └── DatabaseMigrator.kt # Schema-Migrationen
├── discovery/ # Service Discovery
│ └── ServiceRegistration.kt # Service-Registrierung
├── error/ # Fehlerbehandlung
│ └── Result.kt # Result-Type für Fehlerbehandlung
├── serialization/ # Serialisierung
│ └── Serialization.kt # Serialisierungs-Utilities
└── validation/ # Validierung
├── ValidationResult.kt # Validierungsergebnisse
├── ValidationUtils.kt # Validierungs-Utilities
└── ApiValidationUtils.kt # API-Validierung
Core-Domain Komponenten
1. Gemeinsame Enumerationen (Enums.kt)
Zentrale Enumerationen, die modulübergreifend verwendet werden.
PferdeGeschlechtE
enum class PferdeGeschlechtE {
HENGST, // Männlich, nicht kastriert
STUTE, // Weiblich
WALLACH // Männlich, kastriert
}
SparteE (Sportsparten)
enum class SparteE {
DRESSUR, // Dressurreiten
SPRINGEN, // Springreiten
VIELSEITIGKEIT, // Vielseitigkeitsreiten
FAHREN, // Fahrsport
VOLTIGIEREN, // Voltigieren
WESTERN, // Westernreiten
DISTANZ, // Distanzreiten
PARA_DRESSUR, // Para-Dressur
PARA_FAHREN // Para-Fahren
}
DatenQuelleE
enum class DatenQuelleE {
MANUELL, // Manuelle Eingabe
IMPORT, // Datenimport
SYNCHRONISATION, // Externe Synchronisation
MIGRATION // Datenmigration
}
2. Basis-DTOs (BaseDto.kt)
Gemeinsame Basisklassen für Data Transfer Objects.
@Serializable
abstract class BaseDto {
abstract val id: String
abstract val version: Long
abstract val createdAt: Instant
abstract val updatedAt: Instant
}
@Serializable
data class ApiResponse<T>(
val data: T? = null,
val success: Boolean = true,
val message: String? = null,
val errors: List<String> = emptyList(),
val timestamp: Instant = Clock.System.now()
)
@Serializable
data class PagedResponse<T>(
val content: List<T>,
val page: Int,
val size: Int,
val totalElements: Long,
val totalPages: Int,
val hasNext: Boolean,
val hasPrevious: Boolean
)
3. Domain Events (DomainEvent.kt)
Event-Sourcing Infrastruktur für Domain-Driven Design.
interface DomainEvent {
val eventId: Uuid
val aggregateId: Uuid
val eventType: String
val occurredAt: Instant
val version: Long
}
abstract class BaseDomainEvent(
override val eventId: Uuid = uuid4(),
override val aggregateId: Uuid,
override val eventType: String,
override val occurredAt: Instant = Clock.System.now(),
override val version: Long
) : DomainEvent
// Event Publisher Interface
interface DomainEventPublisher {
suspend fun publish(event: DomainEvent)
suspend fun publishAll(events: List<DomainEvent>)
}
// Event Handler Interface
interface DomainEventHandler<T : DomainEvent> {
suspend fun handle(event: T)
fun canHandle(eventType: String): Boolean
}
4. Custom Serializers (Serializers.kt)
Spezialisierte Serializer für Kotlin-Typen.
object UuidSerializer : KSerializer<Uuid> {
override val descriptor = PrimitiveSerialDescriptor("Uuid", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uuid) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Uuid {
return uuidFrom(decoder.decodeString())
}
}
object KotlinInstantSerializer : KSerializer<Instant> {
override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
}
object KotlinLocalDateSerializer : KSerializer<LocalDate> {
override val descriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString())
}
}
Core-Utils Komponenten
1. Fehlerbehandlung (Result.kt)
Funktionale Fehlerbehandlung ohne Exceptions.
sealed class Result<out T, out E> {
data class Success<out T>(val value: T) : Result<T, Nothing>()
data class Failure<out E>(val error: E) : Result<Nothing, E>()
inline fun <R> map(transform: (T) -> R): Result<R, E> = when (this) {
is Success -> Success(transform(value))
is Failure -> this
}
inline fun <R> flatMap(transform: (T) -> Result<R, E>): Result<R, E> = when (this) {
is Success -> transform(value)
is Failure -> this
}
inline fun mapError(transform: (E) -> E): Result<T, E> = when (this) {
is Success -> this
is Failure -> Failure(transform(error))
}
fun isSuccess(): Boolean = this is Success
fun isFailure(): Boolean = this is Failure
fun getOrNull(): T? = when (this) {
is Success -> value
is Failure -> null
}
fun getOrElse(defaultValue: T): T = when (this) {
is Success -> value
is Failure -> defaultValue
}
}
// Extension Functions
inline fun <T> Result<T, *>.onSuccess(action: (T) -> Unit): Result<T, *> {
if (this is Result.Success) action(value)
return this
}
inline fun <E> Result<*, E>.onFailure(action: (E) -> Unit): Result<*, E> {
if (this is Result.Failure) action(error)
return this
}
2. Konfiguration (AppConfig.kt, AppEnvironment.kt)
Zentrale Anwendungskonfiguration.
// AppEnvironment.kt
enum class AppEnvironment {
DEVELOPMENT,
TESTING,
STAGING,
PRODUCTION;
companion object {
fun fromString(env: String): AppEnvironment {
return values().find { it.name.equals(env, ignoreCase = true) }
?: DEVELOPMENT
}
}
}
// AppConfig.kt
data class AppConfig(
val environment: AppEnvironment,
val applicationName: String,
val version: String,
val database: DatabaseConfig,
val redis: RedisConfig,
val kafka: KafkaConfig,
val security: SecurityConfig,
val monitoring: MonitoringConfig
) {
companion object {
fun load(): AppConfig {
val environment = AppEnvironment.fromString(
System.getenv("APP_ENVIRONMENT") ?: "development"
)
return AppConfig(
environment = environment,
applicationName = System.getenv("APP_NAME") ?: "meldestelle",
version = System.getenv("APP_VERSION") ?: "1.0.0",
database = DatabaseConfig.load(),
redis = RedisConfig.load(),
kafka = KafkaConfig.load(),
security = SecurityConfig.load(),
monitoring = MonitoringConfig.load()
)
}
}
}
3. Datenbank-Utilities (DatabaseConfig.kt, DatabaseFactory.kt, DatabaseMigrator.kt)
Datenbank-Abstraktion und -Migration.
// DatabaseConfig.kt
data class DatabaseConfig(
val url: String,
val driver: String,
val username: String,
val password: String,
val maxPoolSize: Int,
val connectionTimeout: Duration,
val migrationEnabled: Boolean
) {
companion object {
fun load(): DatabaseConfig {
return DatabaseConfig(
url = System.getenv("DATABASE_URL") ?: "jdbc:postgresql://localhost:5432/meldestelle",
driver = System.getenv("DATABASE_DRIVER") ?: "org.postgresql.Driver",
username = System.getenv("DATABASE_USERNAME") ?: "meldestelle",
password = System.getenv("DATABASE_PASSWORD") ?: "password",
maxPoolSize = System.getenv("DATABASE_MAX_POOL_SIZE")?.toInt() ?: 10,
connectionTimeout = Duration.ofSeconds(
System.getenv("DATABASE_CONNECTION_TIMEOUT")?.toLong() ?: 30
),
migrationEnabled = System.getenv("DATABASE_MIGRATION_ENABLED")?.toBoolean() ?: true
)
}
}
}
// DatabaseFactory.kt
object DatabaseFactory {
fun create(config: DatabaseConfig): Database {
val hikariConfig = HikariConfig().apply {
jdbcUrl = config.url
driverClassName = config.driver
username = config.username
password = config.password
maximumPoolSize = config.maxPoolSize
connectionTimeout = config.connectionTimeout.toMillis()
}
val dataSource = HikariDataSource(hikariConfig)
return Database.connect(dataSource)
}
}
// DatabaseMigrator.kt
class DatabaseMigrator(private val database: Database) {
suspend fun migrate() {
database.useConnection { connection ->
val flyway = Flyway.configure()
.dataSource(connection.metaData.url, null, null)
.load()
flyway.migrate()
}
}
suspend fun clean() {
database.useConnection { connection ->
val flyway = Flyway.configure()
.dataSource(connection.metaData.url, null, null)
.load()
flyway.clean()
}
}
}
4. Validierung (ValidationUtils.kt, ValidationResult.kt, ApiValidationUtils.kt)
Umfassende Validierungsinfrastruktur.
// ValidationResult.kt
data class ValidationResult(
val isValid: Boolean,
val errors: List<ValidationError> = emptyList()
) {
companion object {
fun valid() = ValidationResult(true)
fun invalid(errors: List<ValidationError>) = ValidationResult(false, errors)
fun invalid(error: ValidationError) = ValidationResult(false, listOf(error))
}
fun and(other: ValidationResult): ValidationResult {
return ValidationResult(
isValid = this.isValid && other.isValid,
errors = this.errors + other.errors
)
}
}
data class ValidationError(
val field: String,
val message: String,
val code: String? = null
)
// ValidationUtils.kt
object ValidationUtils {
fun validateEmail(email: String): ValidationResult {
val emailRegex = "^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$".toRegex()
return if (emailRegex.matches(email)) {
ValidationResult.valid()
} else {
ValidationResult.invalid(ValidationError("email", "Invalid email format"))
}
}
fun validateRequired(value: String?, fieldName: String): ValidationResult {
return if (!value.isNullOrBlank()) {
ValidationResult.valid()
} else {
ValidationResult.invalid(ValidationError(fieldName, "$fieldName is required"))
}
}
fun validateLength(value: String?, fieldName: String, min: Int, max: Int): ValidationResult {
return when {
value == null -> ValidationResult.invalid(ValidationError(fieldName, "$fieldName is required"))
value.length < min -> ValidationResult.invalid(ValidationError(fieldName, "$fieldName must be at least $min characters"))
value.length > max -> ValidationResult.invalid(ValidationError(fieldName, "$fieldName must not exceed $max characters"))
else -> ValidationResult.valid()
}
}
fun validateUuid(value: String?, fieldName: String): ValidationResult {
return try {
if (value != null) {
uuidFrom(value)
ValidationResult.valid()
} else {
ValidationResult.invalid(ValidationError(fieldName, "$fieldName is required"))
}
} catch (e: Exception) {
ValidationResult.invalid(ValidationError(fieldName, "Invalid UUID format"))
}
}
}
5. Service Discovery (ServiceRegistration.kt)
Service-Registrierung für Microservices.
data class ServiceInfo(
val id: String,
val name: String,
val host: String,
val port: Int,
val healthCheckUrl: String,
val tags: Set<String> = emptySet(),
val metadata: Map<String, String> = emptyMap()
)
interface ServiceRegistry {
suspend fun register(serviceInfo: ServiceInfo)
suspend fun deregister(serviceId: String)
suspend fun discover(serviceName: String): List<ServiceInfo>
suspend fun getHealthyServices(serviceName: String): List<ServiceInfo>
}
class ConsulServiceRegistry(private val consulClient: ConsulClient) : ServiceRegistry {
override suspend fun register(serviceInfo: ServiceInfo) {
val service = NewService().apply {
id = serviceInfo.id
name = serviceInfo.name
address = serviceInfo.host
port = serviceInfo.port
tags = serviceInfo.tags.toList()
meta = serviceInfo.metadata
check = NewService.Check().apply {
http = serviceInfo.healthCheckUrl
interval = "10s"
timeout = "5s"
}
}
consulClient.agentServiceRegister(service)
}
override suspend fun deregister(serviceId: String) {
consulClient.agentServiceDeregister(serviceId)
}
override suspend fun discover(serviceName: String): List<ServiceInfo> {
val services = consulClient.getHealthServices(serviceName, true, QueryParams.DEFAULT)
return services.response.map { serviceHealth ->
val service = serviceHealth.service
ServiceInfo(
id = service.id,
name = service.service,
host = service.address,
port = service.port,
healthCheckUrl = "http://${service.address}:${service.port}/actuator/health",
tags = service.tags.toSet(),
metadata = service.meta ?: emptyMap()
)
}
}
override suspend fun getHealthyServices(serviceName: String): List<ServiceInfo> {
return discover(serviceName).filter { service ->
// Additional health check logic if needed
true
}
}
}
6. Serialisierung (Serialization.kt)
JSON-Serialisierung mit Kotlinx Serialization.
object JsonConfig {
val json = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
prettyPrint = false
coerceInputValues = true
useAlternativeNames = false
}
val prettyJson = Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
prettyPrint = true
coerceInputValues = true
useAlternativeNames = false
}
}
inline fun <reified T> T.toJson(): String {
return JsonConfig.json.encodeToString(this)
}
inline fun <reified T> String.fromJson(): T {
return JsonConfig.json.decodeFromString(this)
}
inline fun <reified T> T.toPrettyJson(): String {
return JsonConfig.prettyJson.encodeToString(this)
}
Verwendung in anderen Modulen
Domain Models
// In members-domain
@Serializable
data class Member(
@Serializable(with = UuidSerializer::class)
val memberId: Uuid = uuid4(),
var firstName: String,
var lastName: String,
var email: String,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
fun validate(): ValidationResult {
return ValidationUtils.validateRequired(firstName, "firstName")
.and(ValidationUtils.validateRequired(lastName, "lastName"))
.and(ValidationUtils.validateEmail(email))
}
}
API Responses
// In API Controllers
@RestController
class MemberController {
@GetMapping("/api/members/{id}")
fun getMember(@PathVariable id: String): ApiResponse<Member> {
return try {
val member = memberService.findById(id)
ApiResponse(data = member, success = true)
} catch (e: Exception) {
ApiResponse(
success = false,
message = "Member not found",
errors = listOf(e.message ?: "Unknown error")
)
}
}
}
Result-Type Usage
// In Use Cases
class CreateMemberUseCase {
suspend fun execute(member: Member): Result<Member, ValidationError> {
val validation = member.validate()
return if (validation.isValid) {
try {
val savedMember = memberRepository.save(member)
Result.Success(savedMember)
} catch (e: Exception) {
Result.Failure(ValidationError("system", "Failed to save member"))
}
} else {
Result.Failure(validation.errors.first())
}
}
}
Tests
Unit Tests
class ValidationUtilsTest {
@Test
fun `should validate email correctly`() {
val validEmail = "test@example.com"
val invalidEmail = "invalid-email"
assertTrue(ValidationUtils.validateEmail(validEmail).isValid)
assertFalse(ValidationUtils.validateEmail(invalidEmail).isValid)
}
@Test
fun `should validate required fields`() {
val validValue = "test"
val invalidValue = ""
assertTrue(ValidationUtils.validateRequired(validValue, "field").isValid)
assertFalse(ValidationUtils.validateRequired(invalidValue, "field").isValid)
}
}
class ResultTest {
@Test
fun `should map success result`() {
val result = Result.Success(5)
val mapped = result.map { it * 2 }
assertTrue(mapped.isSuccess())
assertEquals(10, mapped.getOrNull())
}
@Test
fun `should handle failure result`() {
val result = Result.Failure("error")
val mapped = result.map { it * 2 }
assertTrue(mapped.isFailure())
assertNull(mapped.getOrNull())
}
}
Konfiguration
Gradle Dependencies
// core-domain/build.gradle.kts
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1")
api("com.benasher44:uuid:0.8.2")
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
// core-utils/build.gradle.kts
dependencies {
api(project(":core:core-domain"))
implementation("com.zaxxer:HikariCP:5.0.1")
implementation("org.jetbrains.exposed:exposed-core:0.44.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.44.1")
implementation("org.flywaydb:flyway-core:9.22.3")
implementation("com.orbitz.consul:consul-client:1.5.3")
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("org.testcontainers:postgresql:1.19.1")
}
Best Practices
1. Shared Kernel Prinzipien
- Minimale Oberfläche: Nur wirklich gemeinsame Konzepte
- Stabile APIs: Änderungen beeinflussen alle Module
- Versionierung: Sorgfältige Versionierung bei Breaking Changes
- Dokumentation: Umfassende Dokumentation für alle Komponenten
2. Fehlerbehandlung
- Result-Type verwenden: Statt Exceptions für erwartete Fehler
- Validierung: Frühe Validierung mit ValidationResult
- Logging: Strukturiertes Logging für Debugging
3. Serialisierung
- Custom Serializers: Für spezielle Kotlin-Typen
- Versionierung: Schema-Evolution berücksichtigen
- Performance: Effiziente Serialisierung für häufige Operationen
Zukünftige Erweiterungen
- Event Sourcing Enhancements - Erweiterte Event Store Features
- Distributed Tracing - OpenTelemetry Integration
- Metrics Collection - Micrometer Integration
- Configuration Management - Externalized Configuration
- Security Utilities - Encryption/Decryption Utilities
- Caching Abstractions - Cache-Provider Abstractions
- Async Utilities - Coroutines Utilities
- Testing Utilities - Test-Helpers und Fixtures
Letzte Aktualisierung: 25. Juli 2025
Für weitere Informationen zur Gesamtarchitektur siehe README.md.