Caching Strategies for Android
📖 Concept
Caching is one of the most impactful optimizations in mobile apps. A well-designed cache strategy reduces network calls, improves perceived performance, and enables offline functionality.
Caching layers in a typical Android app:
1. In-memory cache (fastest, volatile)
└── LruCache, HashMap, Compose remember
2. Disk cache (persistent, slower)
└── Room database, DataStore, OkHttp cache, DiskLruCache
3. Network cache (HTTP caching)
└── Cache-Control headers, ETag, Last-Modified
Cache invalidation strategies:
- Time-based (TTL): Data expires after a fixed duration. Simple but may serve stale data.
- Event-based: Server pushes invalidation events. Real-time but requires infrastructure.
- Version-based (ETag): Server returns ETag, client sends If-None-Match. 304 = use cache.
- Write-through: Write to cache AND source simultaneously. Always consistent.
- Write-behind: Write to cache first, batch-sync to source later. Fast writes but risk data loss.
The "stale-while-revalidate" pattern:
- Return cached data immediately (fast UX)
- Fetch fresh data from network in background
- Update cache and notify observers (Flow/LiveData emits new data)
- UI updates seamlessly
This is the most common pattern in production Android apps.
💻 Code Example
1// Multi-layer caching implementation23// In-memory LRU cache4class InMemoryCache<K, V>(maxSize: Int) {5 private val cache = object : LruCache<K, V>(maxSize) {6 override fun sizeOf(key: K, value: V): Int = 17 }89 fun get(key: K): V? = cache.get(key)10 fun put(key: K, value: V) = cache.put(key, value)11 fun evict(key: K) = cache.remove(key)12 fun clear() = cache.evictAll()13}1415// Repository with stale-while-revalidate pattern16class ArticleRepository @Inject constructor(17 private val api: ArticleApi,18 private val dao: ArticleDao,19 private val memoryCache: InMemoryCache<String, List<Article>>20) {21 fun getArticles(): Flow<List<Article>> = flow {22 // Layer 1: Memory cache (instant)23 memoryCache.get("articles")?.let { emit(it) }2425 // Layer 2: Database (fast, persistent)26 val dbArticles = dao.getAll().first().map { it.toDomain() }27 if (dbArticles.isNotEmpty()) {28 emit(dbArticles)29 memoryCache.put("articles", dbArticles)30 }3132 // Layer 3: Network (fresh data)33 try {34 val networkArticles = api.getArticles()35 val entities = networkArticles.map { it.toEntity() }36 dao.upsertAll(entities)37 val domainArticles = entities.map { it.toDomain() }38 memoryCache.put("articles", domainArticles)39 emit(domainArticles)40 } catch (e: Exception) {41 if (dbArticles.isEmpty()) throw e42 // Stale data is better than no data43 }44 }.distinctUntilChanged()4546 // Cache-aware single item fetch47 suspend fun getArticle(id: String): Article {48 // Check memory cache first49 memoryCache.get("article_$id")?.let { return it.first() }5051 // Then database52 val entity = dao.getById(id)53 if (entity != null && !entity.isStale()) {54 return entity.toDomain()55 }5657 // Finally network58 val article = api.getArticle(id)59 dao.upsert(article.toEntity())60 return article.toDomain()61 }62}6364// Image caching with Coil (built-in multi-layer cache)65@Composable66fun CachedImage(url: String, modifier: Modifier = Modifier) {67 AsyncImage(68 model = ImageRequest.Builder(LocalContext.current)69 .data(url)70 .memoryCacheKey(url) // In-memory LRU71 .diskCacheKey(url) // Disk LRU72 .crossfade(true)73 .placeholder(R.drawable.placeholder)74 .error(R.drawable.error)75 .build(),76 contentDescription = null,77 modifier = modifier78 )79}
🏋️ Practice Exercise
Practice:
- Implement a 3-layer cache (memory → disk → network) for an API endpoint
- Add TTL-based cache invalidation to your Room entities
- Configure OkHttp HTTP caching with Cache-Control headers
- Implement the stale-while-revalidate pattern with Flow
- Design a cache strategy for an image gallery that handles 10,000+ images
⚠️ Common Mistakes
Not caching at all — every screen load hits the network, wasting bandwidth and battery
Caching without a size limit — unbounded caches consume all available memory/disk
Serving infinitely stale data — cache must have expiration, either time-based or event-based
Using in-memory cache without a disk fallback — data lost on process death
Not cache-busting on user-triggered refresh — pull-to-refresh should bypass cache
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Caching Strategies for Android. Login to unlock this feature.