14 KiB
14 KiB
Meldestelle Development Guidelines
Version: 1.0 Date: 2025-08-15 Status: Active
This document outlines the development guidelines for the Meldestelle project, covering coding conventions, code organization, and testing approaches.
1. Coding Conventions
1.1 Language Standards
- Primary Language: Kotlin (JVM/Multiplatform)
- Java Compatibility: Target Java 21+
- Kotlin Version: Latest stable version
- Code Style: Official Kotlin coding conventions
1.2 Naming Conventions
Classes and Interfaces
// Use PascalCase for classes and interfaces
class MemberService
interface EventRepository
data class MemberRegistration
sealed class AuthResult
// Use descriptive names that reflect domain concepts
class HorseRegistrationService // Good
class HRS // Avoid abbreviations
Functions and Variables
// Use camelCase for functions and variables
fun authenticateUser(): AuthResult
val memberRepository: MemberRepository
suspend fun findByEmail(email: EmailAddress): Result<Member?, RepositoryError>
// Use descriptive test method names with "should" statements
@Test
fun `authenticate should return Success for valid credentials`()
Constants and Enums
// Use SCREAMING_SNAKE_CASE for constants
const val MAX_RETRY_ATTEMPTS = 3
const val DEFAULT_TIMEOUT_MS = 5000L
// Use PascalCase for enum values
enum class MemberStatus {
ACTIVE,
INACTIVE,
SUSPENDED
}
1.3 Code Structure Principles
Result Pattern Usage
// Always use Result pattern for operations that can fail
interface MemberRepository {
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
suspend fun save(member: Member): Result<Unit, RepositoryError>
}
// Result extensions for error handling
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))
}
Coroutines and Async Programming
// Use suspend functions for async operations
suspend fun processEventBatch(events: List<DomainEvent>): Result<Unit, ProcessingError>
// Prefer structured concurrency
class EventProcessor {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
suspend fun processEvents() = withContext(scope.coroutineContext) {
// Implementation
}
}
Documentation Standards
/**
* Authenticates a user with the given credentials.
*
* @param credentials The user credentials containing username and password
* @return AuthResult.Success with user data if authentication succeeds,
* AuthResult.Failure with error details if it fails
*/
suspend fun authenticate(credentials: UserCredentials): AuthResult
2. Code Organization and Package Structure
2.1 Overall Architecture
The project follows a microservices architecture with Domain-Driven Design (DDD) principles and Clean Architecture patterns.
High-Level Structure
Meldestelle/
├── core/ # Shared kernel - fundamental building blocks
│ ├── core-domain/ # Common domain types and interfaces
│ └── core-utils/ # Shared utilities and extensions
├── infrastructure/ # Cross-cutting infrastructure services
│ ├── auth/ # Authentication & authorization
│ ├── messaging/ # Event messaging (Kafka)
│ ├── cache/ # Distributed caching (Redis)
│ ├── gateway/ # API Gateway
│ └── monitoring/ # Observability and monitoring
├── [domain-services]/ # Domain-specific microservices
│ ├── members/ # Member management
│ ├── events/ # Event management
│ ├── horses/ # Horse registry
│ └── masterdata/ # Master data management
├── client/ # Client applications
│ ├── common-ui/ # Shared UI components (KMP)
│ ├── desktop-app/ # Desktop application
│ └── web-app/ # Web application
└── platform/ # Build and dependency management
2.2 Microservice Structure (Clean Architecture)
Each domain service follows a 4-layer architecture:
domain-service/
├── domain-api/ # REST controllers, DTOs, API contracts
├── domain-application/ # Use cases, application logic, orchestration
├── domain-domain/ # Domain models, business rules, interfaces
└── domain-infrastructure/ # Technical implementations (DB, external APIs)
Layer Responsibilities
:domain-api Layer:
// REST Controllers
@RestController
@RequestMapping("/api/v1/members")
class MemberController(private val memberService: MemberService)
// DTOs for external communication
data class MemberRegistrationRequest(
val firstName: String,
val lastName: String,
val email: String
)
:domain-application Layer:
// Use cases and application services
class MemberApplicationService(
private val memberRepository: MemberRepository,
private val eventPublisher: EventPublisher
) {
suspend fun registerMember(command: RegisterMemberCommand): Result<MemberId, MemberError>
}
:domain-domain Layer:
// Domain models and business logic
data class Member(
val id: MemberId,
val personalInfo: PersonalInfo,
val membershipStatus: MembershipStatus
) {
fun activate(): Member = copy(membershipStatus = MembershipStatus.ACTIVE)
}
// Repository interfaces (implemented in infrastructure)
interface MemberRepository {
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
suspend fun save(member: Member): Result<Unit, RepositoryError>
}
:domain-infrastructure Layer:
// Technical implementations
class ExposedMemberRepository(
private val database: Database
) : MemberRepository {
override suspend fun findById(id: MemberId): Result<Member?, RepositoryError> {
// Database implementation using Exposed ORM
}
}
2.3 Package Naming Conventions
// Base package structure
at.mocode.[layer].[domain].[component]
// Examples
at.mocode.members.domain.model // Domain models
at.mocode.members.application.service // Application services
at.mocode.members.infrastructure.persistence // Persistence layer
at.mocode.infrastructure.messaging.kafka // Infrastructure components
at.mocode.core.utils.result // Core utilities
2.4 Dependency Rules
- Core modules must not depend on any other modules
- Domain layer must not depend on infrastructure or application layers
- Application layer can depend on domain layer only
- Infrastructure layer can depend on domain and application layers
- API layer orchestrates calls between application and infrastructure
3. Unit and Integration Testing Approaches
3.1 Testing Strategy Overview
The project follows a comprehensive testing strategy with multiple testing levels:
- Unit Tests - Fast, isolated tests for individual components
- Integration Tests - Tests for component interactions
- Performance Tests - Load and throughput testing
- End-to-End Tests - Full system workflow testing
3.2 Testing Stack
Core Testing Libraries
// Unit testing
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
// Integration testing
testImplementation("org.testcontainers:junit-jupiter:1.19.1")
testImplementation("org.testcontainers:kafka:1.19.1")
testImplementation("org.testcontainers:postgresql:1.19.1")
// Performance testing
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
3.3 Unit Testing Conventions
Test Structure and Naming
class AuthenticationServiceTest {
@BeforeEach
fun setUp() {
// Test setup
}
@Test
fun `authenticate should return Success for valid credentials`() = runTest {
// Given
val credentials = UserCredentials("user@example.com", "validPassword")
coEvery { userRepository.findByEmail(any()) } returns Result.Success(testUser)
// When
val result = authenticationService.authenticate(credentials)
// Then
assertTrue(result is AuthResult.Success)
assertEquals(testUser.id, result.user.id)
}
@Test
fun `authenticate should return Failure for invalid credentials`() = runTest {
// Given - When - Then pattern
}
}
Mocking Best Practices
class MemberServiceTest {
private val memberRepository = mockk<MemberRepository>()
private val eventPublisher = mockk<EventPublisher>()
private val memberService = MemberService(memberRepository, eventPublisher)
@Test
fun `should publish event when member is registered`() = runTest {
// Mock repository responses
coEvery { memberRepository.save(any()) } returns Result.Success(Unit)
coEvery { eventPublisher.publish(any()) } returns Result.Success(Unit)
// Test implementation
val result = memberService.registerMember(validCommand)
// Verify interactions
coVerify { eventPublisher.publish(any<MemberRegisteredEvent>()) }
}
}
3.4 Integration Testing Approaches
Database Integration Tests
@Testcontainers
class MemberRepositoryIntegrationTest {
companion object {
@Container
val postgres = PostgreSQLContainer<Nothing>("postgres:15-alpine")
}
@Test
fun `should persist and retrieve member correctly`() = runTest {
// Test with real database using Testcontainers
val member = createTestMember()
val saveResult = memberRepository.save(member)
assertTrue(saveResult.isSuccess())
val retrievedResult = memberRepository.findById(member.id)
assertTrue(retrievedResult.isSuccess())
assertEquals(member, retrievedResult.getOrNull())
}
}
Messaging Integration Tests
@Testcontainers
class KafkaEventPublisherIntegrationTest {
companion object {
@Container
val kafka = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"))
}
@Test
fun `should publish and consume events correctly`() = runTest {
val event = MemberRegisteredEvent(memberId = MemberId.generate())
val publishResult = eventPublisher.publish(event)
assertTrue(publishResult.isSuccess())
// Verify event was consumed
val consumedEvents = eventConsumer.consumeEvents(timeout = 5.seconds)
assertTrue(consumedEvents.any { it.memberId == event.memberId })
}
}
3.5 Performance Testing
Batch Processing Performance Tests
class KafkaBatchPerformanceTest {
@Test
fun `should process large batches within acceptable time limits`() = runTest {
val batchSize = 1000
val events = generateTestEvents(batchSize)
val startTime = System.currentTimeMillis()
val results = eventProcessor.processBatch(events)
val processingTime = System.currentTimeMillis() - startTime
assertTrue(results.all { it.isSuccess() })
assertTrue(processingTime < 5000) // Should complete within 5 seconds
println("[DEBUG_LOG] Processed $batchSize events in ${processingTime}ms")
}
}
3.6 Test Organization
Directory Structure
src/
├── main/kotlin/ # Production code
└── test/kotlin/ # Test code
├── unit/ # Unit tests (optional sub-organization)
├── integration/ # Integration tests
└── performance/ # Performance tests
Test Categories and Execution
// Use JUnit 5 tags for test categorization
@Tag("unit")
class MemberServiceTest
@Tag("integration")
class MemberRepositoryIntegrationTest
@Tag("performance")
class KafkaBatchPerformanceTest
3.7 Testing Guidelines
Best Practices
- Test Method Naming: Use descriptive names with "should" statements
- AAA Pattern: Arrange, Act, Assert structure
- One Assertion Per Test: Focus on single behavior
- Test Data Builders: Use factory methods for test data creation
- Coroutine Testing: Use
runTestfor suspend functions - Mock Verification: Verify important interactions, not implementation details
Coverage Goals
- Unit Tests: 80%+ code coverage for domain and application layers
- Integration Tests: Cover all repository implementations and external integrations
- Performance Tests: Cover critical batch operations and high-load scenarios
Debugging Support
// Always prefix debug messages with [DEBUG_LOG]
@Test
fun `should handle concurrent requests`() = runTest {
println("[DEBUG_LOG] Starting concurrent request test with ${requestCount} requests")
// Test implementation
println("[DEBUG_LOG] Completed test. Success rate: ${successCount}/${requestCount}")
}
4. Additional Development Standards
4.1 Error Handling
- Use
Resultpattern consistently for operations that can fail - Define domain-specific error types
- Avoid throwing exceptions in domain logic
4.2 Logging and Monitoring
- Use structured logging with appropriate log levels
- Include correlation IDs for request tracing
- Monitor key business metrics and technical performance
4.3 Security Considerations
- Validate all external inputs
- Use JWT tokens for authentication
- Implement proper authorization checks
- Secure sensitive configuration data
This guideline is a living document and should be updated as the project evolves and new patterns emerge.