Mobile system design · 17 min
Mobile system design interview: how the round is scored
How a mobile system design round runs, the seven things a senior answer covers, a worked offline sync example, and how Senior and Staff are scored.
Mobile system design · 12 min read
How to design caching in a mobile system design interview: memory and disk tiers, eviction, invalidation, HTTP caching and the image loader prompt.
This guide is for mobile engineers facing a mobile caching system design interview, a round where caching is the prompt (“design an image loader”) or the deep dive the interviewer picks (“how does your feed cache work?”). It covers the tiers of a mobile cache, how eviction works under memory pressure, how to choose an invalidation strategy, what HTTP gives you for free, a worked image loader design, and the questions the round asks with the answers that score.
The idea that changes the whole answer: the cache on the phone is not there to make things fast. It is a copy of server state that lags behind the server and is sometimes cut off from it entirely. Once you say that, staleness stops being a bug you forgot to fix and becomes a budget you set on purpose, per screen.
The image loader is a common caching prompt on mobile because it touches every tier at once. The prompt usually sounds like: “Cells in a scrolling feed request images by URL. Do not download the same URL twice, and do not waste memory.”
The clarifying questions that change the design:
That last question is rarely asked, and it decides the invalidation half of the design.
A strong answer names the tiers and what each one is allowed to get wrong.
| Tier | What lives there | Lifetime | What it may get wrong |
|---|---|---|---|
| In-memory | Decoded images ready to draw, view models | Dies with the process | Almost nothing; it is the freshest |
| On disk | Encoded image files, cached API responses, the local database | Survives relaunches | Staleness lives longest here |
| HTTP cache | Responses stored by the URL loading system according to server headers | Governed by Cache-Control and validators |
Whatever the server’s headers allow |
| Edge and server caches | CDN, a backend-for-frontend cache | Not yours | Can hand you a stale value before you cache it |
Two platform facts that belong in the answer:
Library/Caches may be purged when the device is low on space. On Android, files in the app’s cache directory may be removed by the system when storage is low. So the disk cache is for things you can re-download, never for the only copy of user data. The user’s unsynced work belongs in the database, not in a cache.Both facts come back later in the round. The first decides what the disk tier may hold; the second decides how the memory tier is sized.
The interviewer will ask what happens when memory runs short.
NSCache evicts automatically under memory pressure and is thread-safe. Its countLimit and totalCostLimit are limits the cache tries to respect, not strict guarantees. Set the cost to the decoded byte size, not to 1 per image.LruCache with sizeOf returning the bitmap’s byte count gives a byte-bounded least-recently-used cache. Respond to memory trim callbacks by shrinking or clearing it.The follow-up is often “implement the LRU yourself”: a hash map for constant-time lookup and a doubly linked list ordered by recency, where each node keeps its key, so evicting the tail can also delete it from the map. Then the concurrency question: an actor gives compile-checked safety but makes every access an await; a lock keeps a synchronous API for a hot path at the cost of manual discipline. Say when each applies.
The general problem: you only learn a cached value is stale by asking the server, and not asking the server is the reason the cache exists. On a phone it is harder: the device is often offline, and its clock cannot be trusted. So do not promise the cache is never stale. Set a staleness budget per screen and decide what the user sees when it is exceeded.
| Data | Staleness budget | Strategy |
|---|---|---|
| Avatars, display names | Hours or a day | Time-to-live, refresh lazily |
| Feeds, counts | Seconds to minutes | Refresh on foreground, show “updated just now” |
| Anything the user acts on: a balance, stock, whether a message still exists | Zero for the decision | Fetch fresh before the action, or make the action safe to be wrong on the server |
The strategies, and when each is right:
A second trap is about who may write the cache. A push arrives carrying an older title while the user’s rename is still pending, writes the list row directly so the notification tap feels fast, and the user watches the new title flip back to the old one. That is not a rendering bug. Two code paths own the same row. Label every copy of a value: authoritative (the server record), replicated (the local database mirror), cached (a copy with an expiry rule), derived (recomputed, never stored as a source) or ephemeral (screen state). Then send every write, whether it comes from a response, a push or a background refresh, through one repository that knows the pending local edits. The push becomes a proposal, not a write.
One trap interviewers like: every client refreshing on the same fixed timer. When the backend recovers from an outage, every cache that expired during it stampedes the server at once. Add jitter to expiry and refresh, the same fix as for retries.
Before building a custom response cache, say what the platform already does. URLSession on iOS uses URLCache, and OkHttp on Android has an HTTP cache you enable with a size and a directory. Both are driven by the server’s headers:
Cache-Control: max-age says how long a response is fresh without asking.ETag lets the client revalidate: it sends If-None-Match, and the server answers 304 Not Modified with no body if nothing changed. That costs a round trip and saves the payload.no-store means do not cache at all, which is what you want for personal or sensitive responses.The senior point is the contract: caching behaviour lives partly in the server’s headers, so it is negotiated with backend, not decided in the client alone. For images at content-addressed URLs, the server can mark them cacheable for a very long time, and invalidation disappears from the design.
Putting it together for the feed prompt:
The follow-ups this design tends to get: “What is your eviction rule?”, “Swap the actor for a class holding a dictionary: what breaks?” (a data race, which Swift 6 strict concurrency refuses to compile), “Two cells show the same URL”, and “Why not use the system’s async image view?” (no cross-view deduplication and little cache control).
| Question | Answer that scores | The trap |
|---|---|---|
| “How big is your memory cache?” | A byte budget based on decoded size, and it shrinks on memory warnings | “200 images” |
| “Where do cached files live?” | The caches directory, knowing the system may purge it; user data never lives there | Putting unsynced user work in a cache |
| “Two cells request the same URL.” | One download, the second request awaits the in-flight task | Two downloads, or a lock held across the network call |
| “The image at a URL changed.” | Freshness rule from headers or a versioned URL; ideally content-addressed URLs | “Clear the cache on every launch” |
| “The user sees a deleted message from cache.” | Staleness budget per screen; data the user acts on is fetched fresh or validated on the server | Treating all cached data the same |
| “A push payload updates the list row directly.” | One write path that reconciles against pending local edits; the push is a proposal | Letting any payload write state outside that path |
| “Why not cache everything forever?” | Disk budget, privacy of cached personal data, and stale data the user acts on | “Storage is cheap” |
| “The backend falls over right after it recovers.” | Synchronised refresh is a thundering herd; jitter expiry and retries | More servers |
| “How do you know the cache works?” | Hit rates per tier, bytes per session, main-thread decode time, memory warnings | “It feels faster” |
Use the table as a drill: cover the middle column, answer each question out loud, then check what you left out. The trap column is what the interviewer is listening for.

Mobile system design · 17 min
How a mobile system design round runs, the seven things a senior answer covers, a worked offline sync example, and how Senior and Staff are scored.

Mobile system design · 14 min
How to design offline-first sync in a mobile system design interview: local source of truth, the outbox, conflict rules, clocks and migration on device.

Mobile system design · 15 min
What an iOS design round adds: background limits, background URLSession, storage, image pipelines and push, with a worked photo feed design.