Skip to content

Instantly share code, notes, and snippets.

@smarteist
Last active August 2, 2025 07:40
Show Gist options
  • Select an option

  • Save smarteist/bd69149f92709c60e6fd0b7d172f7b0b to your computer and use it in GitHub Desktop.

Select an option

Save smarteist/bd69149f92709c60e6fd0b7d172f7b0b to your computer and use it in GitHub Desktop.
Repository caching mechanisms
/**
* A generic, dispatcher-agnostic class for managing data with local and remote sources.
*
* This store provides different caching strategies and returns data via two main methods:
* - `getAsFlow()`: Returns a [Flow] for reactive streams of data.
* - `get()`: Returns a single value via a `suspend` function.
*
* The caller is responsible for specifying the execution context (e.g., using
* `.flowOn(Dispatchers.IO)`) when using the Flow-based API.
*
* @param T The type of data being managed.
* @param cacheRemoteDataSource The remote data source (e.g., API).
* @param cacheLocalDataSource The local data source (e.g., Database).
*/
class CacheStore<T>(
private val cacheRemoteDataSource: CacheRemoteDataSource<T>,
private val cacheLocalDataSource: CacheLocalDataSource<T>,
) {
/** Defines possible caching strategies for reading data. */
enum class Strategy {
/**
* Fetches from remote, updates local cache, and returns the remote result. Does not read
* from the local cache first.
*/
READ_THROUGH,
/**
* Reads from the local cache. If data exists, returns it. Otherwise, fetches from remote,
* updates the cache, and returns the remote result.
*/
READ_BACK,
/**
* For `getAsFlow`: Emits local data, then fetches remote and emits fresh data. For `get`:
* Returns local data, but only after a *blocking* remote refresh.
*/
READ_LAZILY,
}
/** A generic RemoteDataSource interface for fetching data from a remote source (API, etc.). */
interface CacheRemoteDataSource<T> {
suspend fun fetch(params: Map<String, Any?> = emptyMap()): T?
}
/** A generic LocalDataSource interface for reading/writing data in a local cache (DB, etc.). */
interface CacheLocalDataSource<T> {
suspend fun get(params: Map<String, Any?> = emptyMap()): T?
suspend fun put(value: T, params: Map<String, Any?> = emptyMap())
}
// =============================================================================================
// Public APIs
// =============================================================================================
/**
* Returns a [Flow] of data based on the specified [Strategy].
*
* **Important:** This function does not specify a CoroutineDispatcher. The caller is
* responsible for using `.flowOn()` to provide an appropriate dispatcher (e.g.,
* `Dispatchers.IO`).
*
* @param strategy The caching strategy to use.
* @param params A map of dynamic parameters for the data retrieval.
* @return A [Flow] that emits the data.
*/
fun getAsFlow(strategy: Strategy, params: Map<String, Any?> = emptyMap()): Flow<T?> {
return when (strategy) {
Strategy.READ_THROUGH -> readThroughFlow(params)
Strategy.READ_BACK -> readBackFlow(params)
Strategy.READ_LAZILY -> lazyReadFlow(params)
}
}
/**
* Returns a single value based on the specified [Strategy]. This is a suspend function for
* one-shot data retrieval.
*
* @param strategy The caching strategy to use.
* @param params A map of dynamic parameters for the data retrieval.
* @return A single nullable value of type [T].
*/
suspend fun get(strategy: Strategy, params: Map<String, Any?> = emptyMap()): T? {
return when (strategy) {
Strategy.READ_THROUGH -> performReadThrough(params)
Strategy.READ_BACK -> performReadBack(params)
// Works as single shot
Strategy.READ_LAZILY -> lazyReadSingleShot(params)
}
}
// =============================================================================================
// Flow Implementations
// =============================================================================================
private fun lazyReadFlow(params: Map<String, Any?>): Flow<T?> = flow {
// 1. Emit local data immediately
val localData = cacheLocalDataSource.get(params)
emit(localData)
// 2. Fetch remote, update local, and emit the new data
try {
val remoteData = cacheRemoteDataSource.fetch(params)
if (remoteData != null) {
cacheLocalDataSource.put(remoteData, params)
// Emit the fresh data from the source of truth (the cache)
emit(cacheLocalDataSource.get(params))
}
} catch (ex: Exception) {
ex.printStackTrace()
}
}
private fun readThroughFlow(params: Map<String, Any?>): Flow<T?> = flow {
// Wrap around flow
emit(performReadThrough(params))
}
private fun readBackFlow(params: Map<String, Any?>): Flow<T?> = flow {
// Wrap around flow
emit(performReadBack(params))
}
// =============================================================================================
// Suspend Function Implementations
// =============================================================================================
/**
* Implements the [Strategy.READ_LAZILY] for the non-flow `get` method. It returns the original
* local data, but only *after* waiting for the remote refresh to finish.
*/
private suspend fun lazyReadSingleShot(params: Map<String, Any?>): T? {
val localData = cacheLocalDataSource.get(params)
// This call is blocking; it will complete before the function returns.
refreshCache(params)
return localData
}
/** Core logic for the [Strategy.READ_THROUGH] strategy. */
private suspend fun performReadThrough(params: Map<String, Any?>): T? {
return try {
val remoteData = cacheRemoteDataSource.fetch(params)
remoteData?.let { cacheLocalDataSource.put(it, params) }
remoteData
} catch (ex: Exception) {
ex.printStackTrace()
null
}
}
/** Core logic for the [Strategy.READ_BACK] strategy. */
private suspend fun performReadBack(params: Map<String, Any?>): T? {
val localData = cacheLocalDataSource.get(params)
// If local data exists, return it. Otherwise, perform a read-through.
return localData ?: performReadThrough(params)
}
/** Fetches from remote and updates the local cache. Used by [lazyReadSingleShot]. */
private suspend fun refreshCache(params: Map<String, Any?>) {
try {
val remoteData = cacheRemoteDataSource.fetch(params)
remoteData?.let { cacheLocalDataSource.put(it, params) }
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment