What caching strategy do you use for API responses on mobile — HTTP caching, ETags, cache-aside?
The right answer layers three different caches — HTTP-level, conditional requests, and an app-level cache-aside — because each one solves a different problem.
LEARN BY DOINGPut your intuition to work3 steps
What caching strategy do you use for API responses on mobile — HTTP caching, ETags, cache-aside?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Three layers, three different jobs
"Caching strategy" sounds like one decision, but on a mature mobile client it's actually three layers stacked on top of each other, each solving a problem the other two don't: HTTP-level caching saves you from re-downloading a response at all, conditional requests save you bandwidth when you must ask again, and an app-level cache-aside layer is what actually gives your UI offline access to data as domain objects rather than raw HTTP bytes.
#Layer 1: HTTP caching does the cheapest job for free
If your backend sends proper Cache-Control headers, OkHttp's built-in disk cache will serve GET requests straight from disk without touching the network at all, as long as the response is still fresh. This costs you almost nothing to wire up and it's the first thing to get right before reaching for anything fancier.
val cacheSize = 20L * 1024 * 1024 // 20 MBval cache = Cache(File(context.cacheDir, "http_cache"), cacheSize)val client = OkHttpClient.Builder().cache(cache).build()
This only helps for GET requests, and only if the server actually sends Cache-Control: max-age=.... A server that sends no-store or nothing at all gives OkHttp nothing to work with, and you'll hit the network every time regardless of how big your cache directory is.
#Layer 2: conditional requests turn a cache miss into a cheap 304
Once the cached response is stale (past max-age), the naive move is a full re-fetch. A better one is a conditional request: if the server sent an ETag (a fingerprint of the resource) or Last-Modified, OkHttp automatically attaches If-None-Match or If-Modified-Since on the revalidation request. If the resource hasn't actually changed, the server responds 304 Not Modified with an empty body, and OkHttp transparently serves the cached copy — you save the full payload transfer even though you technically made a network round trip.
// You rarely write this by hand — OkHttp does it automatically when the// cached response carries an ETag and Cache-Control says it needs// revalidation (e.g. "max-age=0, must-revalidate"). This is what it// looks like on the wire://// Request: GET /articles/42// If-None-Match: "a1b2c3-etag"// Response: 304 Not Modified (no body — cached copy is still valid)
The interplay matters for an interview answer: max-age controls whether you ask at all; ETag/Last-Modified control how expensive asking is once you do.
#Layer 3: cache-aside is where your domain model actually lives
HTTP caching operates on raw responses and is invisible to your app code — great for bandwidth, useless the moment you need to show "last known good" data as a typed domain object, merge it with local state, or support GraphQL (where, as covered earlier, there's no per-resource URL to cache against at all). Cache-aside fixes that: the repository checks a local persisted store first, returns what it has immediately, and separately triggers a network fetch that updates that store — with the UI observing the store reactively so it picks up the fresh data the moment it lands.
class ArticleRepository(private val dao: ArticleDao,private val api: ArticleApi,) {fun observeArticle(id: String): Flow<Article> = flow {// 1. Emit whatever we already have, instantly — even if stale.dao.getArticle(id)?.let { emit(it.toDomain()) }// 2. Refresh from the network in the background.try {val fresh = api.getArticle(id)dao.upsert(fresh.toEntity())} catch (e: IOException) {// Offline or the request failed — the cached emission above// already gave the UI something to render.}}.flatMapLatest {// 3. Re-emit from the DB, which now reflects the refreshed value.dao.observeArticle(id).map { it.toDomain() }}}
This is the "stale-while-revalidate" feel most apps want: the screen paints immediately from whatever's on disk, then quietly updates in place once the network responds — and it keeps working with zero code changes if the device is offline entirely, because step 1 already rendered something.
#Choosing the mix
🧠 Check your understanding
0/1 · 0/1 answered1. A GET request's cached response has passed its max-age, but the server still has an ETag for it. What happens on the next request if the resource hasn't actually changed server-side?