refactor(infra-cache): Refine module with Kotlin idioms and robust tests

This commit introduces a comprehensive refactoring of the cache module to improve code consistency, API ergonomics, and test robustness.

Code Refinements & Improvements
Standardized on kotlin.time: Replaced all usages of java.time.Instant and java.time.Duration with their kotlin.time counterparts (Instant, Duration). This aligns the module with the project-wide standard established in the core module and avoids type conversions.

Added Idiomatic Kotlin API: Introduced inline extension functions with reified type parameters for get() and multiGet(). This allows for a cleaner, more type-safe call syntax (e.g., cache.get<User>("key")) for Kotlin consumers.

Code Cleanup: Removed redundant @OptIn(ExperimentalTime::class) annotations from data classes by setting the compiler option at the module level in cache-api/build.gradle.kts.

Testing Enhancements
Stabilized Offline-Mode Tests: Re-implemented the previously disabled offline capability tests. The new approach uses MockK to simulate RedisConnectionFailureException instead of trying to stop/start the Testcontainer. This allows for reliable and robust testing of the "dirty key" synchronization logic.

Fixed Compilation Errors: Resolved various compilation errors in the test suite that arose from the type refactoring and incorrect mock setups.
This commit is contained in:
stefan
2025-08-09 14:57:44 +02:00
parent 4f67379b42
commit 7592adfbb5
10 changed files with 207 additions and 579 deletions
+39 -58
View File
@@ -2,75 +2,56 @@
## Überblick
Das **Cache-Modul** stellt eine zentrale und wiederverwendbare Caching-Infrastruktur für alle Microservices des Meldestelle-Systems bereit. Caching ist eine entscheidende Technik zur Verbesserung der Anwendungsleistung, zur Reduzierung der Latenz und zur Entlastung von Backend-Systemen wie der primären PostgreSQL-Datenbank.
Das **Cache-Modul** stellt eine zentrale, hochverfügbare und wiederverwendbare Caching-Infrastruktur für alle Microservices bereit. Es dient der Verbesserung der Anwendungsperformance, der Reduzierung von Latenzen und der Entlastung der primären PostgreSQL-Datenbank.
## Architektur: Port-Adapter-Muster
Das Modul folgt streng dem **Port-Adapter-Muster** (auch als Hexagonale Architektur bekannt), um eine saubere Trennung zwischen der Caching-Schnittstelle (dem "Port") und der konkreten Implementierung (dem "Adapter") zu gewährleisten.
Das Modul folgt streng dem **Port-Adapter-Muster** (Hexagonale Architektur), um eine saubere Trennung zwischen der Caching-Schnittstelle (dem "Port") und der konkreten Implementierung (dem "Adapter") zu gewährleisten.
* **`:infrastructure:cache:cache-api`**: Definiert den abstrakten "Vertrag" für das Caching (`DistributedCache`-Interface), ohne sich um die zugrunde liegende Technologie zu kümmern. Die Fach-Services programmieren ausschließlich gegen dieses Interface.
* **`:infrastructure:cache:redis-cache`**: Die konkrete Implementierung des Vertrags, die **Redis** als hochperformantes Caching-Backend verwendet. Kapselt die gesamte Redis-spezifische Logik.
infrastructure/cache/
├── cache-api/ # Der "Port": Definiert die Caching-Schnittstelle
└── redis-cache/ # Der "Adapter": Implementiert die Schnittstelle mit Redis
## Schlüsselfunktionen
* **Offline-Fähigkeit & Resilienz:** Das Modul verfügt über einen In-Memory-Cache, der bei einem Ausfall der Redis-Verbindung als Fallback dient. Schreib-Operationen werden lokal als "dirty" markiert und automatisch mit Redis synchronisiert, sobald die Verbindung wiederhergestellt ist.
* **Idiomatische Kotlin-API:** Bietet neben der Standard-API auch ergonomische Erweiterungsfunktionen mit `reified`-Typen für eine saubere und typsichere Verwendung in Kotlin-Code (`cache.get<User>("key")`).
* **Projekweite Konsistenz:** Verwendet `kotlin.time.Duration` und `kotlin.time.Instant` für eine einheitliche Handhabung von Zeit- und Dauer-Angaben im gesamten Projekt.
* **Automatisierte Verbindungsüberwachung:** Überprüft periodisch den Zustand der Redis-Verbindung und informiert Listener über Statusänderungen (`CONNECTED`, `DISCONNECTED`).
### `cache-api`
## Verwendung
Dieses Modul ist der **abstrakte Teil** der Architektur. Es definiert den "Vertrag" für das Caching, ohne sich um die zugrunde liegende Technologie zu kümmern.
Ein Microservice bindet `:infrastructure:cache:redis-cache` als Abhängigkeit ein und lässt sich das `DistributedCache`-Interface per Dependency Injection geben.
* **Zweck:** Definiert ein oder mehrere Interfaces, z.B. `CacheService`, mit generischen Methoden wie `get(key)`, `set(key, value, ttl)` und `evict(key)`.
* **Vorteil:** Jeder Service im System programmiert nur gegen dieses Interface. Die Geschäftslogik ist vollständig von der Caching-Technologie entkoppelt. Ein Austausch des Caching-Providers (z.B. von Redis zu Caffeine) würde keine Änderungen in den Fach-Services erfordern.
**Beispiel mit der idiomatischen Kotlin-API:**
```kotlin
@Service
class MasterdataService(
private val cache: DistributedCache // Nur das Interface wird verwendet!
) {
fun findCountryById(id: String): Country? {
val cacheKey = "country:$id"
### `redis-cache`
Dieses Modul ist die **konkrete Implementierung** der in `cache-api` definierten Schnittstellen.
* **Zweck:** Stellt eine Spring-basierte Konfiguration und eine Implementierung des `CacheService`-Interfaces bereit, die **Redis** als Datenspeicher verwendet. Es nutzt Spring Data Redis und den Lettuce-Client für die Kommunikation.
* **Technologie:** Verwendet Jackson für die Serialisierung der zu cachenden Objekte in das JSON-Format, bevor sie in Redis gespeichert werden.
* **Vorteil:** Kapselt die gesamte Redis-spezifische Logik an einem einzigen Ort.
## Verwendung in anderen Modulen
Ein Microservice, der Caching nutzen möchte, geht wie folgt vor:
1. **Abhängigkeit deklarieren:** Das Service-Modul (z.B. `masterdata-service`) fügt eine `implementation`-Abhängigkeit zu `:infrastructure:cache:redis-cache` in seiner `build.gradle.kts` hinzu.
```kotlin
// In masterdata-service/build.gradle.kts
dependencies {
implementation(projects.infrastructure.cache.redisCache)
}
```
2. **Interface injizieren:** Im Service-Code wird nur das Interface aus `cache-api` per Dependency Injection angefordert, nicht die konkrete Redis-Klasse.
```kotlin
// In einem Use Case oder Service
@Service
class MasterdataService(
private val cache: CacheService // Nur das Interface wird verwendet!
) {
fun findCountryById(id: String): Country? {
val cacheKey = "country:$id"
// 1. Versuche, aus dem Cache zu lesen
val cachedCountry = cache.get<Country>(cacheKey)
if (cachedCountry != null) {
return cachedCountry
}
// 2. Wenn nicht im Cache, aus der DB lesen
val dbCountry = countryRepository.findById(id)
// 3. Ergebnis in den Cache schreiben für zukünftige Anfragen
if (dbCountry != null) {
cache.set(cacheKey, dbCountry, ttl = 3600) // Cache für 1 Stunde
}
return dbCountry
// 1. Versuche, aus dem Cache zu lesen (typsicher und sauber)
val cachedCountry = cache.get<Country>(cacheKey)
if (cachedCountry != null) {
return cachedCountry
}
// 2. Wenn nicht im Cache, aus der DB lesen
val dbCountry = countryRepository.findById(id)
// 3. Ergebnis in den Cache schreiben für zukünftige Anfragen
dbCountry?.let {
cache.set(cacheKey, it, ttl = 1.hours) // Cache für 1 Stunde
}
return dbCountry
}
```
}
```
Diese Architektur stellt sicher, dass die Geschäftslogik sauber und von Infrastrukturdetails unberührt bleibt.
## Testing-Strategie
Die Qualität des Moduls wird durch eine zweistufige Teststrategie sichergestellt:
---
**Letzte Aktualisierung**: 31. Juli 2025
* **Integrationstests mit Testcontainers: Die Kernfunktionalität wird gegen eine echte Redis-Datenbank getestet, die zur Laufzeit in einem Docker-Container gestartet wird. Dies garantiert 100%ige Kompatibilität.**
* **Unit-Tests mit MockK: Die komplexe Logik der Offline-Fähigkeit und Synchronisation wird durch das Mocking des RedisTemplate getestet. So können Verbindungsausfälle zuverlässig simuliert werden, ohne den Test-Lebenszyklus zu stören.**