๐Ÿ”Œ APIs, Databases, and Backend Integration

Caching Strategies for Android Apps

Caching makes apps faster, cheaper and usable offline, and it's also where stale-data bugs come from. This guide covers the cache layers available on Android, the main strategies, and how to decide freshness and invalidation for each kind of data.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Advanced
On this page
  1. Cache layers on Android
  2. Core strategies
  3. Stale-while-revalidate with Room
  4. HTTP caching with OkHttp
  5. In-memory caches
  6. Images
  7. Invalidation rules
  8. Policy per data type: an example
  9. Common mistakes
  10. Frequently asked questions
  11. Conclusion

"There are only two hard things in computer science: cache invalidation and naming things." On Android, caching is also essential: users on slow networks, data caps and flaky connections notice every unnecessary request. The goal is a deliberate policy per data type, not caching everything.

Cache layers on Android

Layered caching from memory to network A request first checks the in-memory cache, fastest but lost when the process dies; then the persistent cache in Room or files, which survives restarts; then the OkHttp HTTP disk cache, which respects server headers; and finally the network. Responses populate the layers on the way back. Memorynanoseconds Room / filessurvives restarts HTTP cacheOkHttp, headers Networkslowest, costs data Faster โ† โ†’ fresher Each layer trades freshness for speed and availability.
You rarely need all four for the same data. Choose per data type.

Core strategies

StrategyBehaviourUse for
Cache-firstUse cache if present; network only when missingStatic or rarely changing data (country lists, config)
Network-firstTry network; fall back to cache on failureData that must be fresh but should degrade offline (account balance with "last updated")
Stale-while-revalidateShow cache immediately, refresh in background, update UIFeeds, lists, profiles: most content
Network-onlyNever cachePayments, OTPs, highly sensitive or one-time data

Stale-while-revalidate with Room

ProfileRepository.ktKotlin
class ProfileRepository(
    private val api: ProfileApi,
    private val dao: ProfileDao,
    private val clock: Clock,
) {
    private val maxAge = Duration.ofMinutes(10)

    /** Emits cached profile immediately; refreshes in the background if stale. */
    fun observeProfile(userId: String): Flow<Profile?> = dao.observe(userId)
        .onStart { refreshIfStale(userId) }
        .map { it?.toDomain() }

    private suspend fun refreshIfStale(userId: String) {
        val cached = dao.get(userId)
        val stale = cached == null ||
            Duration.between(Instant.ofEpochMilli(cached.fetchedAt), clock.instant()) > maxAge
        if (stale) {
            runCatching { api.profile(userId) }
                .onSuccess { dao.upsert(it.toEntity(fetchedAt = clock.millis())) }
            // On failure, keep serving the cached copy; the UI can show "last updated".
        }
    }
}

Running the refresh in onStart delays the first emission until the refresh attempt finishes. To show the cache instantly, launch the refresh in a separate coroutine from the ViewModel instead.

HTTP caching with OkHttp

If your server sends proper Cache-Control and ETag headers, OkHttp's disk cache handles freshness and conditional requests (304 Not Modified) automatically:

OkHttp cacheKotlin
val client = OkHttpClient.Builder()
    .cache(Cache(directory = File(context.cacheDir, "http_cache"), maxSize = 50L * 1024 * 1024))
    .build()

// Offline fallback: accept stale cached responses when there is no connectivity
class OfflineCacheInterceptor(private val isOnline: () -> Boolean) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        if (!isOnline()) {
            request = request.newBuilder()
                .cacheControl(CacheControl.Builder().onlyIfCached().maxStale(7, TimeUnit.DAYS).build())
                .build()
        }
        return chain.proceed(request)
    }
}

HTTP caching is great for GET responses you display as-is. When you need to query, combine or modify data locally, Room is the better cache.

In-memory caches

Small LRU cacheKotlin
class ExchangeRateCache(maxEntries: Int = 50) {
    private val cache = LruCache<String, Pair<Double, Long>>(maxEntries)

    fun get(pair: String, maxAgeMs: Long, now: Long): Double? =
        cache.get(pair)?.takeIf { now - it.second < maxAgeMs }?.first

    fun put(pair: String, rate: Double, now: Long) { cache.put(pair, rate to now) }
}

Memory caches are fast but vanish with the process, so use them for hot, recomputable data. Respond to onTrimMemory by clearing large ones.

Images

Image libraries such as Coil and Glide provide memory and disk caching, downsampling and request deduplication. Don't build your own. Make sure image URLs change when content changes (versioned URLs), or use cache headers correctly.

Invalidation rules

  • Time-based (TTL): simple and predictable. Choose per data type.
  • Event-based: after a user edits their profile, update or invalidate the cache immediately.
  • Server-driven: ETags and versions, or push messages that trigger a refresh.
  • On logout: clear all user-specific caches, including the HTTP cache if it contains personal data.

Policy per data type: an example

DataStrategyTTL / trigger
Product catalogueStale-while-revalidate (Room)1 hour; pull-to-refresh
Prices and stockNetwork-firstAlways refresh on product open
User profileStale-while-revalidate10 minutes; invalidate on edit
App configCache-firstRefresh on app start in background
Payment statusNetwork-only-

Common mistakes

  • Caching without any invalidation, so users see outdated data indefinitely.
  • Different screens caching the same data separately, so they disagree.
  • Personal data left in caches after logout.
  • Unbounded caches that grow until storage or memory runs out.

Frequently asked questions

Room cache or HTTP cache?

HTTP cache for responses you show directly and that the server controls with headers. Room when you need queries, joins, offline edits or a single source of truth across screens.

Where should cache logic live?

In repositories or data sources, so every caller gets consistent freshness.

How do I show staleness to users?

Store fetchedAt with cached data, and display "Updated 5 min ago" when it matters, for example for balances and prices.

Conclusion

Effective caching is per data type: choose cache-first, network-first, stale-while-revalidate or network-only; pick the right layer (memory, Room, HTTP); and define clear invalidation, including on logout.

Next step: secure the credentials behind those requests with Secure Token Storage, or move on to quality with Unit Testing in Android with JUnit.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp