avdar
2.0.1indexedType-safe caching with CBOR-encoded models, stale-while-revalidate serving, L1 in-memory LRU plus L2 persistent backing, request deduplication, cache-control parsing and configurable TTL policies.
Type-safe caching with CBOR-encoded models, stale-while-revalidate serving, L1 in-memory LRU plus L2 persistent backing, request deduplication, cache-control parsing and configurable TTL policies.
Avdar is a Kotlin Multiplatform (KMP) caching library that provides a type-safe API, a small DSL for cache configuration, and built-in stale-while-revalidate (SWR) behavior. It offers L1 (in-memory) and L2 (persistent) caching, request deduplication, and cache-control integration to keep data fast and resilient across platforms.
There is a Mongolian saying: "It's better to look in the chest than ask around" (Айлаас эрэхээр авдраа уудал). That saying captures the essence of this library: looking in your own "chest" (cache) before making a network request.
@Serializable models and CBOR encoding via kotlinx.serialization.Each cache entry has two timestamps:
Avdar layers two cache tiers:
LruMemoryStore for fast in-memory access with LRU eviction.Store implementation that you provide (for example, Room, SQLDelight, or file-based storage).You choose between public and private stores per entity. Private stores are intended for sensitive data (often encrypted and/or with hashed keys).
Entity cache policies (TTL and SWR durations) can be persisted via PolicyStore. Call persistPolicies() after registration to store policy metadata for later inspection or tooling.
dependencies {
implementation("im.nmds.avdar:avdar:1.0.0")
}
@Serializable
data class Region(val id: String, val name: String)
val avdar = Avdar(
stores = AvdarStores(
publicStore = publicStoreImpl,
privateStore = privateStoreImpl,
policyStore = policyStoreImpl,
memoryStore = LruMemoryStore()
)
)
avdar.entity(Region::class) {
store = StoreType.Public
ttl = minutes
swr = days
}
avdar.persistPolicies()
region = avdar.fetch<Region>(key = ) {
response = api.getRegion()
putAndReturn(cacheControl = response.cacheControl) { response.value }
}
avdar.entity(User::class) {
store = StoreType.Private
ttl = 10.minutes
swr = 1.hours
typeName = "users"
}
val user = avdar.fetch<User>(key = "user-42") {
val response = api.getUser("user-42")
putAndReturn(cacheControl = response.cacheControl) { response.value }
}
Fetch order:
val fresh = avdar.fetch<User>(key = "user-42", forceRefresh = true) {
val response = api.getUser("user-42")
putAndReturn { response.value }
}
forceRefresh = true bypasses freshness checks but still deduplicates concurrent requests and falls back to stale data on error.
val fresh = avdar.refetch<User>(key = "user-42") {
val response = api.getUser("user-42")
putAndReturn { response.value }
}
refetch is a dedicated helper that always executes the fetch block and updates the cache.
val cached = avdar.get<User>("user-42")
val exists = avdar.exists<User>("user-42")
val fresh = avdar.isFresh<User>("user-42")
avdar.put("user-42", user)
avdar.invalidate<User>("user-42")
Avdar includes a small helper to parse HTTP cache directives and ETags:
val cacheControl = CacheControl.parse(
headerValue = response.headers["Cache-Control"],
etag = response.headers["ETag"]
)
Use cacheControl.maxAge, cacheControl.staleWhileRevalidate, cacheControl.noStore, and cacheControl.etag inside putAndReturn or put to override default TTL/SWR behavior.
You provide two implementations:
Each store implements the Store interface:
A PolicyStore implementation is optional but recommended for persistable TTL/SWR metadata.
LruMemoryStore provides an in-memory L1 cache with LRU eviction. The default target size is 10 MB, and you can adjust it:
val memoryStore = LruMemoryStore(targetSize = 25L * 1024 * 1024) // 25 MB
Avdar is silent by default. To enable logging, pass an AvdarLogger implementation to the constructor:
You can integrate with any logging framework (Kermit, SLF4J, Logback, etc.) by implementing AvdarLogger.
Entities must be serializable via kotlinx.serialization (CBOR). Make sure your models are annotated with @Serializable and that you include the serialization plugin.
./gradlew :library:jvmTest
Apache-2.0
interface Store {
suspend fun getEntry(type: String, key: String): CacheEntry?
suspend fun setEntry(entry: CacheEntry)
suspend fun deleteEntry(type: String, key: String)
suspend fun deleteStaleEntries(currentTimeMillis: Long)
suspend fun deleteAllEntries()
}
val avdar = Avdar(
stores = AvdarStores(
publicStore = publicStoreImpl,
privateStore = privateStoreImpl,
policyStore = policyStoreImpl,
memoryStore = LruMemoryStore()
),
logger = object : AvdarLogger {
override fun d(message: () -> String) = println("[DEBUG] ${message()}")
override fun i(message: () -> String) = println("[INFO] ${message()}")
override fun w(message: () -> String) = println("[WARN] ${message()}")
override fun w(throwable: Throwable, message: () -> String) = println("[WARN] ${message()}: $throwable")
override fun e(message: () -> String) = println("[ERROR] ${message()}")
override fun e(throwable: Throwable, message: () -> String) = println("[ERROR] ${message()}: $throwable")
}
)
Surfaced from shared tags and platforms — no rankings paid for.