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:
+39
-58
@@ -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.**
|
||||
|
||||
Reference in New Issue
Block a user