
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 = 15.minutes
swr = 7.days
}
// Persist policies after registering all entities
avdar.persistPolicies()
// Fetch with SWR
val region = avdar.fetch<Region>(key = "region-123") {
val response = api.getRegion("region-123")
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:
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()
}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 MBAvdar is silent by default. To enable logging, pass an AvdarLogger implementation to the constructor:
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")
}
)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:jvmTestApache-2.0
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 = 15.minutes
swr = 7.days
}
// Persist policies after registering all entities
avdar.persistPolicies()
// Fetch with SWR
val region = avdar.fetch<Region>(key = "region-123") {
val response = api.getRegion("region-123")
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:
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()
}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 MBAvdar is silent by default. To enable logging, pass an AvdarLogger implementation to the constructor:
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")
}
)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:jvmTestApache-2.0