Free Android System Design Interview Questions & Answers

Offline-first, sync, pagination, and mobile design tradeoffs.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. What does 'offline-first' actually mean for an Android app, and what is the key architectural decision it forces?

    Offline-first means the UI reads from and writes to a local store first, treating the network as a background sync mechanism rather than the primary data source. The key decision is making the local database the single source of truth: the UI observes the local store (for example a Room Flow) and never blocks on the network, while a separate layer reconciles local state with the server. Naive apps fetch from the network and cache as a convenience, so they break when offline; a true offline-first app keeps working because every read and write path terminates locally and sync is decoupled from user actions. The tradeoff is added complexity around conflict resolution and eventual consistency, which you accept in exchange for a responsive, resilient experience.

  2. Why is Room (or any local DB) commonly chosen as the single source of truth instead of exposing repository methods that return network results directly?

    Because a single source of truth gives you one authoritative place the UI observes, so any write, whether from the user or a sync job, produces a consistent stream of updates via reactive queries. If the repository returned network results directly, you would have two competing states, the in-flight network response and whatever is cached, and reconciling them per screen leads to flicker and stale UI. With Room as the source of truth, the network layer's only job is to write fresh data into the DB, and the UI automatically re-renders from the observed query. The tradeoff is you must design your schema and sync writes carefully, but you gain deterministic UI state and trivial offline support.

  3. When designing a paginated feed, what is the practical difference between offset-based and cursor-based pagination, and why does offset break?

    Offset pagination asks for a page by numeric position, such as skip 40 take 20, while cursor pagination asks for items after an opaque pointer tied to a stable sort key like an id or timestamp. Offset breaks when items are inserted or deleted between page fetches: a new item at the top shifts every subsequent offset, so the user sees duplicates or skips items as they scroll. Cursor pagination is immune because it anchors to a specific record rather than a position, so inserts above the cursor do not affect what comes after it. The tradeoff is cursors cannot jump to an arbitrary page number and require a stable, indexed sort key, but for infinite feeds that is exactly what you want.

  4. How does Paging 3's RemoteMediator combine a network source with a local cache, and what problem does it solve that a plain PagingSource does not?

    RemoteMediator lets you page from the network into a local Room database while the UI pages from the database via a PagingSource, so Room remains the single source of truth and the feed works offline. It solves the problem of showing cached pages instantly on cold start and appending fresh pages from the network only when the user scrolls past the loaded boundary, using load types Refresh, Prepend, and Append. A plain PagingSource pages directly from one source with no persistence, so it cannot survive process death or offline use gracefully. The tradeoff is you must manage remote keys, typically a separate table mapping items to next and previous cursors, to know where to resume network paging.

  5. In an image loading library like Coil or Glide, why is there both a memory cache and a disk cache, and what goes in each?

    The memory cache holds decoded, ready-to-draw bitmaps in an LRU structure so re-displaying a recently seen image is instant with no decode cost, while the disk cache stores the original encoded bytes so you can re-decode without a network round trip. They serve different layers: memory optimizes for CPU and latency but is tiny and volatile, disk optimizes for bandwidth and survives restarts but requires a decode. A naive single cache either wastes RAM storing raw bytes or wastes CPU re-decoding constantly. The tradeoff is tuning sizes, since an oversized memory cache causes memory pressure and GC churn while an undersized one thrashes and re-decodes.

  6. Why is downsampling critical when loading images, and what happens if you load a full-resolution photo into a small ImageView?

    A bitmap's memory cost is width times height times bytes per pixel, independent of file size, so a 4000 by 3000 photo at 4 bytes per pixel consumes about 48 MB in RAM regardless of the JPEG being 2 MB on disk. If you load it into a 200 pixel view without downsampling you pay that full 48 MB and risk OutOfMemory, plus slower decode and GC pressure. Downsampling decodes at a reduced sample size so the bitmap matches the target dimensions, cutting memory by the square of the scale factor. Libraries like Coil and Glide do this automatically by inspecting the target size, which is a major reason to use them rather than BitmapFactory directly.

  7. What is the difference between an FCM notification message and a data message, and why does the distinction matter for delivery and handling?

    A notification message contains a notification payload that the system tray displays automatically when the app is backgrounded, and your code only runs if the user taps it, whereas a data message always delivers to your onMessageReceived handler regardless of app state so you control the behavior. The distinction matters because notification messages are convenient but you lose control while backgrounded, while data messages let you do custom work like syncing but are subject to background execution limits and Doze. A common trap is expecting onMessageReceived to fire for a notification message while backgrounded, which it does not. For reliable custom handling you send data-only messages, and for guaranteed tray display with minimal effort you use notification messages, sometimes combining both.

  8. What delivery guarantee does FCM provide, and how should a well-designed app treat push messages given that guarantee?

    FCM provides best-effort, at-most-once-ish delivery: messages can be dropped, delayed by Doze or throttling, or collapsed, and there is no guarantee every message arrives or arrives in order. Therefore push should be treated as a hint to sync rather than as the payload of record; the correct pattern is a lightweight data message that tells the app to fetch authoritative state from your server. If you rely on push to carry the actual data, missed or collapsed messages cause silent data loss and inconsistency. The tradeoff is an extra fetch round trip, but it makes the system self-healing because any successful sync reconciles whatever pushes were lost.

  9. When would you choose polling over a persistent connection like a WebSocket for real-time-ish updates on mobile?

    You choose polling when updates are infrequent or latency-tolerant, when you want to conserve battery by not holding a socket open, or when the backend does not support server push, because polling is stateless, simple, and works through any proxy. A persistent WebSocket gives low latency and instant server-to-client push but consumes battery and radio keeping the connection alive and requires reconnect and heartbeat logic. On mobile, holding an idle socket keeps the radio in a high-power state, so for something like a feed that updates every few minutes, periodic polling or FCM-triggered fetch is more efficient. The rule of thumb is use push or sockets for genuinely interactive real-time features like chat, and polling or FCM for periodic freshness.

  10. How does cursor pagination interact with a pull-to-refresh that inserts new items at the top of a feed?

    Pull-to-refresh fetches the newest items before the current head cursor, so you need a separate 'prepend' path that queries for items newer than your topmost cursor rather than reusing the append cursor. The subtlety is gaps: if many items were added since the last load, a single refresh page may not reach the previously loaded head, leaving a hole in the timeline. Paging 3 handles this with the invalidation and remote-key model, where a refresh can clear cached pages or you detect and represent the gap explicitly so the user can load the missing range. The tradeoff is between simplicity, clearing and reloading from the top, and continuity, stitching new items onto existing ones without losing scroll position.

  11. What is stale-while-revalidate, and why is it a good default for mobile caching?

    Stale-while-revalidate means you immediately show the cached copy even if it is past its freshness window, while asynchronously fetching an update in the background and swapping it in when it arrives. It is a good mobile default because it makes the UI feel instant and works offline, yet still converges to fresh data without the user staring at a spinner. The alternative, blocking on the network until fresh data arrives, wastes the cache and punishes users on flaky networks. The tradeoff is the user may briefly see slightly stale content, so it fits feeds and profiles well but not strongly consistent data like account balances, where you may prefer to block or show an explicit staleness indicator.

  12. How do ETags and conditional requests reduce bandwidth, and what does the server return on a match?

    The server sends an ETag, an opaque version identifier, with a response; the client stores it and sends it back on the next request in an If-None-Match header, and if the resource is unchanged the server returns 304 Not Modified with no body. This saves bandwidth because the client revalidates cheaply and reuses its cached copy instead of re-downloading identical bytes, which matters on metered mobile connections. The naive approach re-downloads the full payload every time just to check freshness. The tradeoff is a round trip is still required to revalidate, so ETags reduce payload size but not request count; pairing them with a TTL lets you skip revalidation entirely while data is known-fresh.

  13. Why do you need jitter in addition to exponential backoff when retrying failed requests?

    Exponential backoff spaces out retries by doubling the delay, but if many clients failed at the same moment, for example after a server blip, they all back off by the same amounts and retry in synchronized waves that re-overload the server, a thundering herd. Jitter adds randomness to each delay so retries spread out over time instead of colliding, smoothing the load. Full jitter, picking a random delay between zero and the current backoff ceiling, is usually the most effective. The tradeoff is slightly less predictable timing per client, but the system-wide stability gain is essential; backoff without jitter is a common and subtle mistake.

  14. What is request coalescing, and where is it valuable in a mobile client?

    Request coalescing means detecting that multiple callers are asking for the same in-flight resource and letting them share a single network request rather than each firing its own. It is valuable when several UI components or rapid retries request the same endpoint, such as multiple views needing the current user profile on startup, or a screen that re-requests on every recomposition. Without coalescing you waste bandwidth, battery, and server capacity on duplicate work and can create race conditions between responses. The implementation typically keeps a map of in-progress requests keyed by URL or query, returning the shared deferred result; the tradeoff is managing that map's lifecycle and cancellation correctly.

  15. In a chat app, why can you not rely on client wall-clock timestamps for message ordering, and what is a common fix?

    Client clocks are unsynchronized and can drift, be wrong, or be manipulated, so ordering by device time causes messages to appear out of order across participants, especially near midnight or across time zones. A common fix is to order by a server-assigned sequence number or a server timestamp applied at ingestion, giving a single authoritative order all clients agree on. For local optimistic display you show the message immediately with a provisional position, then reconcile to the server order once acknowledged. The tradeoff is a brief reordering when the ack arrives, which you smooth by keeping pending messages visually grouped at the bottom until confirmed.

  16. How does a local outbox queue enable reliable offline sending in a chat app, and what state does each message carry?

    An outbox queue persists each outgoing message locally the instant the user sends it, marking it pending, and a background worker drains the queue by transmitting messages when connectivity returns, updating state to sent, delivered, or failed. Each message carries a client-generated id, its content, a status field, and a retry count, so the UI can show sending, sent, and failed states and offer retry. This makes sends durable across offline periods and process death because the source of truth is the local store, not an in-memory request. The tradeoff is you must handle ordering and deduplication, since a message may be transmitted, the ack lost, and the message retried.

  17. What is an idempotency key in the context of message sends or payments, and what failure does it prevent?

    An idempotency key is a unique client-generated identifier attached to a request so the server can recognize a retry of the same logical operation and return the original result instead of performing it twice. It prevents duplicate side effects when a request succeeds on the server but the response is lost to a flaky network and the client retries, which otherwise creates two messages or double charges. The server stores the key with the operation's result for a window and short-circuits repeats. The tradeoff is the server must persist keys and define a dedup window, but it turns an at-least-once transport into effectively exactly-once semantics from the user's perspective.

  18. How would you design resumable large uploads so a 500 MB video survives a dropped connection?

    You split the file into fixed-size chunks and upload them individually, tracking which chunks the server has acknowledged so that after a drop you resume from the first unacknowledged chunk instead of restarting. Protocols like tus or a custom chunked API let the client query the server for the current received offset and continue from there. The client persists upload progress and the upload id locally, and a WorkManager job with a network constraint drives it across app restarts. The tradeoffs are added complexity in chunk bookkeeping and server-side assembly, plus choosing a chunk size that balances retry cost against per-request overhead, but the payoff is uploads that reliably complete on unreliable mobile networks.

  19. Why should an analytics client batch events and buffer them offline, and what delivery semantic does this imply?

    Sending each analytics event as its own request wastes battery and radio wake-ups and loses events when offline, so a good client buffers events in a local store and flushes them in batches on a schedule, when the buffer fills, or on constraints like unmetered network. This implies at-least-once delivery: because the app may crash or the network may fail after sending but before confirming, the client retries a batch, so the backend must deduplicate on an event id. The tradeoff is possible duplicate events, which you accept because losing events is worse for analytics and dedup is cheap server-side. Batching also lets you piggyback on other wake-ups, cutting battery cost significantly.

  20. What is the difference between remote config and feature flags, and why run a staged rollout through them?

    Remote config delivers server-controlled values, such as thresholds, copy, or endpoints, that tune behavior without an app update, while feature flags are boolean or variant gates that turn features on or off for segments of users; feature flags are essentially a use case of remote config. A staged rollout enables a feature for a small percentage first, watches crash and engagement metrics, and ramps up only if healthy, so a bad feature is caught before it reaches everyone and can be killed instantly without an app store release. The tradeoff is added conditional complexity and the need to test both flag states, plus ensuring the default value is safe for users who fetch config late or never.

  21. What is a cold start on Android, and what are the main phases where time is spent?

    A cold start happens when the app process does not exist and the system must create it from scratch, which is the slowest launch type compared to warm and hot starts. Time is spent forking and initializing the process, running Application.onCreate including all eager initialization and library setup, then creating and laying out the first Activity and its initial frame. The common trap is doing heavy synchronous work in Application.onCreate or in dependency-injection graph construction, which delays the first frame. You optimize by deferring non-critical initialization off the startup path, using lazy initialization and the App Startup library, and measuring with the time-to-initial-display and time-to-full-display metrics.

  22. Why is WorkManager preferred for deferrable background sync, and how do constraints improve battery efficiency?

    WorkManager is preferred because it guarantees deferrable work runs eventually even across app restarts and reboots, respecting system background limits and Doze, which raw threads or AlarmManager do not handle cleanly. Constraints like requires-unmetered-network, requires-charging, or battery-not-low let the system batch your work with other apps' work into shared maintenance windows, so the radio and CPU wake once for many tasks instead of many times. This coalescing is the core battery win because keeping the radio idle matters more than the transfer itself. The tradeoff is you give up precise timing, so WorkManager is right for sync and uploads but not for exact-time or immediate user-facing tasks.

  23. What is last-write-wins conflict resolution, and what is its central weakness for multi-device sync?

    Last-write-wins resolves a conflict by keeping whichever update has the latest timestamp and discarding the other, which is simple and requires only a per-record modified time. Its central weakness is silent data loss: if two devices edit the same record while offline, the earlier edit is thrown away entirely even if the changes touched different fields and could have merged. It also depends on comparable clocks, so clock skew can let a stale write win. The tradeoff is simplicity versus correctness; last-write-wins is fine for low-conflict, single-user-mostly data, but for collaborative or field-level merges you need versioning, per-field resolution, or CRDTs to avoid discarding real work.

  24. Why do you need tombstones to sync deletes, and what problem do they solve that a plain delete does not?

    A tombstone is a marker record that says an item was deleted rather than actually removing the row, so the deletion itself can propagate during sync. Without it, if device A deletes a record and simply drops the row, device B still has the record and, seeing A lacks it, may treat it as missing data and resurrect it back to A, so the delete never sticks. The tombstone carries the deleted id and a timestamp so all devices converge on the deletion. The tradeoff is tombstones accumulate and must be garbage collected after a safe retention window, once you are confident every device has synced past that point, otherwise the local store grows unbounded.

  25. How would you design a typeahead search that is responsive without hammering the backend?

    You debounce keystrokes so a request fires only after the user pauses, typically 200 to 300 milliseconds, cancel the in-flight request when a newer query arrives, and require a minimum query length before searching. You cache recent query results, often in memory keyed by the query string, so backtracking or repeated prefixes serve instantly, and you can prefetch or serve local results first. The subtle correctness issue is out-of-order responses, where a slower earlier request lands after a faster later one and overwrites correct results; you guard against this by tagging responses with the query and discarding stale ones. The tradeoff is latency from debouncing versus request volume, tuned to the backend's capacity and the feature's feel.

  26. What is optimistic UI with rollback, and what must you preserve to roll back correctly?

    Optimistic UI applies a change to the local state and shows it immediately, before the server confirms, then reconciles when the response arrives: on success it keeps the change, on failure it rolls back to the prior state and surfaces an error. To roll back correctly you must preserve the previous value or a reversible description of the change, plus enough identity to locate the affected item, because by the time the failure arrives other state may have moved. The benefit is a snappy, network-independent feel; the tradeoff is complexity and the risk of a confusing fl– back if rollback is visible, so you design it to be rare and gentle, for example re-inserting a like with a brief toast rather than a jarring jump.

  27. Where should access and refresh tokens be stored on Android, and why not in plain SharedPreferences?

    Tokens should be stored in a way backed by the Android Keystore, for example encrypting them with a Keystore-held key or using EncryptedSharedPreferences, so the key material never leaves secure hardware and the stored bytes are useless if extracted. Plain SharedPreferences writes tokens as clear text in an app-private XML file, which is readable on a rooted device or via a device backup, exposing long-lived credentials. The refresh token especially deserves protection because it can mint new access tokens. The tradeoff is Keystore-backed crypto adds a little complexity and the encrypted store can be invalidated if the lock screen changes, but the security gain over cleartext is decisive.

  28. How should a mobile client handle silent access-token refresh when many requests fail with 401 at once?

    The client should detect a 401, pause outgoing requests, refresh the token exactly once, and then retry the failed requests with the new token, rather than letting every concurrent 401 trigger its own refresh. A single-flight guard, often a mutex around the refresh so only the first caller refreshes while others await the result, prevents a storm of refresh calls that can race and even invalidate each other if the server rotates refresh tokens. An OkHttp Authenticator or interceptor is the usual place to implement this. The tradeoff is careful concurrency handling, but without it you get token thrashing, redundant refreshes, and users unexpectedly logged out.

  29. What is a Backend for Frontend (BFF), and what problem does it solve for a mobile client?

    A BFF is a server-side layer dedicated to one client type, here mobile, that aggregates and reshapes data from downstream microservices into exactly what the app screen needs in a single response. It solves the chattiness and over-fetching problem where a mobile client would otherwise make many round trips and stitch data itself, wasting battery and latency on a high-latency radio. The BFF also lets you version and evolve the mobile contract independently and push transformation off the device. The tradeoff is another service to build and operate and the risk of it becoming a bloated dumping ground, so you keep it thin and client-specific rather than turning it into a second business-logic tier.

  30. On mobile, what are the real tradeoffs between GraphQL and REST, beyond the usual talking points?

    GraphQL lets the client request exactly the fields it needs in one round trip, which directly reduces over-fetching and the number of high-latency requests that hurt on mobile, and it evolves without versioned endpoints. The costs are harder HTTP caching because most queries are POSTs to one endpoint, the risk of expensive client-crafted queries, and more complex client tooling and error handling. REST is simpler, caches naturally via URLs and ETags, and is easy to reason about, but tends toward over-fetching or a proliferation of custom endpoints. The pragmatic answer for mobile is often a BFF that offers tailored endpoints, or GraphQL when screens have highly variable data needs and you can invest in caching and query cost controls.

  31. What are partial responses and field selection, and why do they matter more on mobile than on desktop?

    Partial responses let the client ask for only a subset of a resource's fields, via a fields parameter in REST or field selection in GraphQL, so the payload carries just what the screen renders. They matter more on mobile because bandwidth is metered and often slow, radio time drains battery, and parsing large JSON on a constrained device costs CPU and memory. Fetching a full user object to show a name and avatar wastes all three. The tradeoff is a more complex API contract and caching that must account for varying field sets, but for data-heavy screens on cellular the savings in latency, battery, and data cost are substantial.

  32. How do you estimate the local storage a feed cache will consume, and why does the estimate guide your eviction policy?

    You estimate by multiplying the average serialized size of one item, including text and any thumbnails, by the number of items you intend to retain, then adding index and overhead; for example 2 KB per post times 5000 cached posts is about 10 MB, plus images which dominate if cached locally. This estimate guides eviction because unbounded caches eventually trigger low-storage warnings or get cleared by the system, so you cap the cache by count or bytes and evict least-recently-used entries. The tradeoff is a smaller cache means more refetching and weaker offline coverage, while a larger one risks the OS reclaiming your data, so you size it to the offline experience you promise and measure real item sizes rather than guessing.

  33. How would you estimate the bandwidth cost of a polling design, and when does that estimate push you toward push instead?

    You estimate bandwidth as request size plus response size times poll frequency times active users; for instance a 2 KB response polled every 30 seconds is roughly 240 KB per user per hour even when nothing changed, most of it wasted revalidation. That estimate pushes you toward push when the update rate is much lower than the poll rate, because you are paying constant cost for rare changes, both in server load and device battery from radio wake-ups. Push or FCM-triggered fetch turns that into cost only when data actually changes. The tradeoff is push infrastructure complexity and best-effort delivery, so you weigh the wasted-poll math against the engineering cost and the latency requirements.

  34. What is the difference between WebSocket and Server-Sent Events, and when is SSE the better fit on mobile?

    WebSocket is a full-duplex connection where both client and server send frames freely, while SSE is a one-way server-to-client stream over a long-lived HTTP response with built-in automatic reconnection and event ids for resuming. SSE is the better fit when you only need server-to-client updates, like a live feed or notifications, because it is simpler, rides on plain HTTP so it traverses proxies easily, and reconnects natively. WebSocket is warranted when the client also streams data upward frequently, such as interactive chat or collaborative editing. The tradeoff is SSE cannot send client-to-server over the same channel and is text-oriented, so choosing it commits you to a separate path for client uploads, which is fine when those are infrequent.

  35. How do you design reconnect and backoff for a real-time socket so it is both prompt and battery-friendly?

    On disconnect you reconnect with exponential backoff plus jitter, starting from a small delay and capping at a ceiling, and you reset the backoff after a stable connection so transient blips recover quickly while persistent outages do not spin. You gate reconnection on actual connectivity using the network callback rather than blindly retrying with no network, and you tie the socket's lifecycle to app foreground state so a backgrounded app does not hold a socket and drain battery. Heartbeats detect half-open connections but should be as infrequent as correctness allows. The tradeoff is prompt recovery versus battery and server load, and the jitter is essential so a server restart does not bring every client back simultaneously.

  36. What is a vector clock, and what does it capture that a single last-modified timestamp cannot?

    A vector clock is a map from each replica or device to a counter it increments on every update, so comparing two vectors tells you whether one update causally happened before another or whether they are concurrent. A single timestamp cannot distinguish causality from coincidence: it tells you which is later in wall time but not whether one edit was made with knowledge of the other, so it silently loses one side of a genuine concurrent conflict. Vector clocks let you detect concurrency and then apply a real merge or surface the conflict rather than blindly picking a winner. The tradeoff is added storage and complexity that grows with the number of participants, so they suit multi-device or collaborative sync where correct conflict detection justifies the cost.

  37. How would you design read receipts and delivery receipts in chat without flooding the network?

    You distinguish delivery, the message reached the recipient's device, from read, the user actually viewed it, and you send each as a lightweight ack keyed by message id, coalescing multiple acks into a single batched update rather than one request per message. To avoid flooding you debounce and batch receipts, for example sending the highest read message id per conversation on a short interval or when the screen loses focus, since read state is monotonic and you only need the latest position. The server fans out the receipt to other participants, often over the same real-time channel. The tradeoff is a small delay in receipt visibility versus a large reduction in traffic, which is worth it because receipts are high-volume and low-urgency.

  38. How does the local store serve as the queue and source of truth for an at-least-once analytics pipeline, and where can duplicates arise?

    Events are written to a local table the moment they occur, each with a unique id and timestamp, and a flusher reads a batch, transmits it, and deletes those rows only after a success response, so a crash or network failure leaves the batch intact for retry. Duplicates arise when the server processes a batch and persists it but the acknowledgment is lost, so the client, still seeing the rows, resends them; the server therefore deduplicates on the event id. This is the classic at-least-once outcome of using local persistence as the durable queue. The tradeoff is you accept possible duplicates in exchange for never losing events, and you keep event ids stable and idempotent so server-side dedup is straightforward.

  39. How do you keep state consistent across a user's multiple devices when each can edit offline?

    You give the server an authoritative, monotonically increasing version or sequence per record and have each device sync deltas since its last known version, applying a defined conflict-resolution rule when a device's base version is stale. Field-level versioning or CRDTs let non-overlapping edits from two devices merge without loss, while last-write-wins is simpler but discards one side. Deletes propagate via tombstones so an offline device does not resurrect removed data, and a per-device sync cursor tells the server where to resume. The tradeoff is the more correct the merge, the more metadata and logic you carry; you pick the resolution strategy per data type, using strong merges for user content and simple rules for low-value or single-writer fields.

  40. What observability signals should a mobile app emit for a network-heavy feature, and why is client-side alone insufficient?

    The app should emit request success and error rates by endpoint, latency percentiles, retry counts, cache hit ratios, and payload sizes, plus breadcrumbs leading to crashes and ANRs, so you can see the real user-perceived behavior across device and network diversity. Client-side telemetry alone is insufficient because it only reports from sessions that survived to send data, missing crashes before flush and offline periods, and it lacks the server's view of load, so you correlate client and server metrics. Crash reporting tools capture stack traces and device context, while custom events capture the feature funnel. The tradeoff is telemetry itself costs battery and bandwidth, so you sample and batch, balancing insight against the very resource costs you are trying to observe.

  41. Why is crash-free session rate a better health metric than raw crash count, and what blind spot do both share?

    Crash-free session rate normalizes crashes against usage, so it stays comparable as your user base grows or as traffic fluctuates, whereas a raw count can rise simply because more people use the app or fall during a quiet period, misleading you about actual stability. It directly reflects the fraction of user experiences that were clean, which maps to user impact. The blind spot both share is that crashes are only hard failures; they miss ANRs, silent data loss, non-fatal errors, and degraded experiences that never crash but still frustrate users. So you complement crash-free rate with ANR rate, non-fatal exception tracking, and user-perceived latency to see failures that leave the process alive.

  42. How would you design a global rate limiter's client-side cooperation so the client respects server limits gracefully?

    The server communicates limits via response headers, such as a Retry-After on a 429 and remaining-quota headers, and the client honors them by pausing until the indicated time rather than blindly retrying, combining that with exponential backoff and jitter for unspecified cases. The client can also self-throttle proactively by coalescing duplicate requests, batching, and caching so it simply makes fewer calls, and it should surface a graceful degraded state instead of hammering. The subtle trap is treating 429 like a transient error and retrying immediately, which deepens the overload. The tradeoff is added client logic and slightly delayed operations, but cooperative clients keep the whole system stable and avoid being throttled harder or banned.

  43. How do you handle a paginated feed when the underlying data can be reordered by ranking, not just appended chronologically?

    Ranked feeds break simple cursor pagination because the sort key can change between page fetches, so an item might appear on two pages or none as ranking shifts; the fix is to make the server compute a stable feed session, snapshotting or seeding the ranking so pagination within one session is consistent. The client passes a session or seed token alongside the cursor, and the server pages within that frozen view, issuing a new session on pull-to-refresh. This trades absolute freshness for coherent scrolling, because within a session the user sees a stable ordering even as the live ranking evolves underneath. The alternative, ranking live on every page fetch, produces duplicates and gaps, so most large feeds use a per-session materialized or seeded ordering.

  44. What is the tradeoff between storing images in the same database versus a separate file or disk cache, and which is correct?

    Storing large binary images as blobs in your relational or document database bloats it, slows queries and backups, and wastes the row cache on bytes you never query, so the correct pattern is to store image files on disk or in a dedicated cache and keep only references, URLs or file paths, in the database. Libraries like Coil manage the disk cache as encoded bytes keyed by URL, decoupled from your entity store. Keeping references in the DB preserves fast metadata queries while the file system handles bulk bytes efficiently. The tradeoff is you must coordinate two stores, evicting cached files when references are removed and handling missing files gracefully, but this separation is standard because databases are poor blob stores.

  45. How would you architect end-to-end so a user's action feels instant on a flaky network yet remains eventually consistent with the server?

    You apply the action optimistically to the local single-source-of-truth store so the UI updates immediately, enqueue a durable pending operation in an outbox, and let a background worker with retry and backoff sync it when connectivity allows, using an idempotency key so retries do not duplicate. On success you clear the pending flag; on a definitive failure you roll back the local state and inform the user, while transient failures just keep retrying. This gives instant feel plus eventual convergence because every action is durably recorded locally and reconciled server-side. The tradeoffs are conflict handling when the server rejects or has newer state, and careful UI treatment of pending and failed items so users understand what is not yet confirmed.

  46. How do you decide TTL values for different caches in one app, and why is a single global TTL a mistake?

    TTL should reflect how fast each data type changes and how costly staleness is, so you set short TTLs or revalidation for volatile, high-stakes data like prices or availability and long TTLs for stable data like a user's profile or static config. A single global TTL is a mistake because it either makes stable data revalidate needlessly, wasting bandwidth and battery, or lets volatile data go dangerously stale; one number cannot fit both. You often combine TTL with ETag revalidation and stale-while-revalidate so expiry triggers a cheap background check rather than a blocking refetch. The tradeoff is per-type tuning adds configuration and requires understanding each data's change rate, but it is what keeps the cache both fresh where it matters and cheap where it does not.

  47. How would you design sync to be efficient with a delta or incremental approach rather than full refetches?

    Instead of refetching entire collections, the client stores a sync token or a since-timestamp and asks the server for only records changed after it, receiving creates, updates, and tombstoned deletes, then advances the token; this cuts bandwidth and battery dramatically for large, slowly changing datasets. The server must track change ordering, typically a monotonic version or change log per record, and handle the first sync as a bounded full snapshot. The subtle correctness point is atomicity of the token, you only advance it after successfully applying the whole delta, so an interrupted sync resumes cleanly without gaps. The tradeoff is more server-side machinery, a change log and tombstone retention, versus the naive simplicity and heavy cost of full refetches.

  48. What are the consistency and correctness pitfalls of optimistic UI when several dependent operations are queued offline?

    When operations depend on each other, for example creating an item then editing then deleting it, applying them optimistically offline can produce a local state the server cannot reproduce if an earlier operation fails or if the server assigns a real id that later operations must reference. You must preserve operation order, map client-generated temporary ids to server-assigned ids as acks arrive, and roll back or repair the whole dependent chain on failure rather than a single step. Idempotency keys and a strictly ordered outbox are essential. The tradeoff is significant complexity, so many apps constrain what can be done offline or collapse redundant queued operations, for example canceling a create-then-delete pair locally, to keep the reconciliation tractable.

  49. How would you design multi-device consistency for something like read state across phones, tablets, and web without heavy conflict handling?

    Read state is monotonic and idempotent, so you can model it as a per-conversation last-read position that only ever moves forward, and each device pushes and pulls the maximum read id; conflicts resolve trivially by taking the larger value, needing no vector clocks or merges. The server stores the authoritative max per user and fans out changes over the real-time channel so all devices converge. This exploits the data's structure, a commutative max operation, to avoid general conflict resolution entirely. The tradeoff is this elegance only applies to data with a natural monotonic or commutative shape; for arbitrary editable content you still need versioning or CRDTs, so the design lesson is to model state so conflicts become mathematically impossible where you can.

  50. How would you design and validate the whole offline-first sync system to converge correctly, and what makes this genuinely hard to get right?

    You make the local store the source of truth, represent every mutation as a durable, idempotent, ordered operation with a stable id, resolve conflicts with a strategy chosen per data type ranging from last-write-wins to field-level versioning or CRDTs, propagate deletes with tombstones, and sync incrementally via a per-device cursor, then prove convergence by ensuring operations are commutative or ordered so any device applying the same set reaches the same state. It is genuinely hard because failures are partial and asymmetric: acks get lost, clocks skew, operations arrive reordered or duplicated, and process death interrupts multi-step syncs, so correctness only holds if every layer is idempotent and every conflict path is defined rather than incidental. You validate with adversarial testing, simulating network partitions, delayed and duplicated deliveries, and concurrent edits, and by asserting eventual state equality across devices. The core tradeoff is that stronger convergence guarantees demand more metadata, more logic, and more testing, so you invest that rigor only where data value and multi-device editing justify it.

Practice all Android System Design questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics