Mobile system design · 15 min read

Android system design interview: designing for the platform

What an Android design round adds: process death, WorkManager, Doze, FCM priority and Room as source of truth, with a worked chat design and its follow-ups.

An Android system design round uses the same skeleton as any mobile design round (see Mobile system design interview), and then tests whether the design survives Android. The process can be killed whenever it is in the background. Background work is deferred by Doze and rationed by standby buckets. A push message can arrive while the app is not running. An answer that ignores those facts reads as a web design with a phone drawn around it.

Where Android answers usually break

Area The common wrong answer What Android does
State “The ViewModel holds it” ViewModels do not survive process death
Background work “A background thread syncs every 10 minutes” Work is deferred in Doze; standby buckets limit how often an app runs
Long transfers “Start a service” Background-started foreground services are restricted; services need a declared type
Push “FCM delivers the message instantly” High priority messages are for user-visible events and can be deprioritised if misused
Storage “SharedPreferences for everything” Room for records, DataStore for preferences, Keystore-backed keys for secrets

The platform facts that shape the design

Process death is normal. Android kills background processes to reclaim memory. When the user returns, the system restores the back stack and saved instance state, not your in-memory objects. Anything the user would be upset to lose is written to disk as it changes.

WorkManager is the default for deferrable, guaranteed work. It persists work across process death and reboots, respects constraints (network, charging), and applies backoff. It does not run work at an exact time. For “sync when the network comes back”, it is the right answer.

Doze and App Standby Buckets. On a stationary device with the screen off, Doze batches network access and jobs into maintenance windows. Standby buckets (active, working set, frequent, rare, restricted) set how often an app’s jobs may run, based on how recently the user used it. A design that assumes regular background runs must say what happens to a user in the rare bucket.

Foreground services are for work the user is aware of, with a notification, and must declare a type (required since Android 14). Starting one from the background is restricted. For a user-initiated large upload with visible progress, a foreground service or a long-running worker in WorkManager is appropriate; for silent sync, it is not.

FCM message priority. High priority messages can wake the device from Doze and are meant for time-sensitive, user-visible events such as a chat message. Using high priority for messages that do not result in a notification can get them deprioritised. Normal priority messages may wait for a maintenance window.

Android system design interview questions

These are Android system design interview questions as they are usually posed, with the platform detail each one is really testing. The product is the wrapper; the Android behaviour underneath is what gets scored.

Prompt The Android detail it tests The answer that scores
“Design a chat app with offline send.” Process death and guaranteed delivery Messages written to Room before they show as sent; WorkManager sends them when the network returns
“Design a photo or file upload.” Work the user can see versus work they cannot A user-initiated upload with progress runs as a long-running worker with a notification; silent sync stays deferrable
“Design a news feed that works offline.” Room as the source of truth The UI observes the database; the network only updates it, so the feed survives the process being killed
“Design notifications for this app.” FCM priority and Doze High priority only for user-visible events; normal priority waits for a maintenance window
“Design background sync for a to-do app.” Standby buckets and constraints WorkManager with a network constraint, and a design that is correct for a user in the rare bucket
“Design a long form the user fills in over several minutes.” Saved state and process death Input persisted as it changes; the screen restores from saved state and disk, not from the ViewModel alone

Each of these is answered with the platform facts above. The detail that decides many Android rounds is process death: say it before the interviewer does, and show where each piece of state survives it.

Worked example: a chat client with offline send

Prompt: “Design the Android client for a one-to-one chat app. Messages must be sendable offline.”

Requirements

One-to-one only, text first, images later. Messages appear on the other device within seconds when both are online. Message history syncs across the user’s devices. Scope out: group chat, end-to-end encryption (mention it as a later decision that changes the server’s role).

Architecture

  • Room as source of truth. Tables for conversations and messages. The UI observes Flows from Room queries and never waits for the network to draw.
  • Sending: the app writes the message to Room with a client-generated id and status sending, in one transaction. A SyncWorker is enqueued with a network constraint, as unique work so repeated sends do not create parallel workers.
  • The worker sends pending messages in order, each with its client id as an idempotency key, marks them sent with the server timestamp, and retries with exponential backoff on failure.
  • Receiving in the foreground: a WebSocket connection while the app is visible, owned by a component tied to the process lifecycle, closed when the app goes to the background.
  • Receiving in the background: an FCM high priority message for each new chat message. The app shows a notification from the payload and enqueues a sync to fetch the full history delta. The notification does not depend on the sync succeeding. If the product forbids message text in a push, the notification shows generic text and the app fetches the content.
  • Ordering: messages are ordered by server sequence once acknowledged; pending messages sort after the last acknowledged one by local order. Say this explicitly, because the follow-up will ask. Keep sending, accepted, delivered and read as separate states, and let each checkmark mean exactly one of them.

Retries, classified

WorkManager guarantees that the worker runs again. It does not guarantee that the worker remembers where it stopped, and the process can die in the middle of doWork(). So progress lives in Room, never in the worker’s memory. And not every failure is retried. Sort each response into four classes: retryable (transport errors, 503, 429: back off and resend under the same client id), terminal (the server rejected the message: mark it failed and show it), unknown (the request may have landed: resend with the same id, never a new one), and user-actionable (the session needs a new sign-in: stop and ask). A worker that backs off on everything retries a rejected message forever.

Process death, tested

The user types a long message, switches to another app, and the process dies. The draft survives only if it was saved: a draft column in Room updated as the user types (debounced), or SavedStateHandle for short text. If you debounce, say how many keystrokes are unsaved in the gap. Say how you would test it: “Don’t keep activities” only destroys activities, so it is not process death. Background the app and run adb shell am kill <package>, then reopen it. Force-stop is a third case, and it clears pending work.

Observability

Time from send to server acknowledgement, count of messages stuck in sending for more than a minute, WorkManager failure counts by reason, notification delivery versus open rates, and crash and ANR rates from Play vitals.

Follow-ups to expect

Follow-up What it tests A strong answer
“The phone is in Doze for hours.” Knowing the platform limits High priority FCM for visible messages; history catches up on the next window or when opened
“Two messages sent offline arrive out of order.” Ordering design Client ids, server sequence after ack, local order only for pending
“The same message is sent twice after a retry.” Idempotency The server deduplicates by client id
“The server rejects one message for good.” Failure classification Terminal outcomes stop the retries; the message shows as failed with a reason
“Add image messages.” Large transfers Store the file locally, upload in a worker with progress, send the message once the upload has an id
“The user reinstalls the app.” Source of truth The server holds history; the local database is a cache that rebuilds
“The process dies after the server accepts a message, before the app records the answer.” The lost response Resend under the same client id and let the server return the existing message. Without server deduplication, no amount of backoff fixes it
“The sync cursor has expired.” History recovery Resync from the server and keep the pending local messages instead of replacing the database
“The user signs out with messages pending.” Account ownership Pending messages belong to the account that wrote them; the next account never sends them with its own credentials

Senior and Staff

A Senior answer designs this well. A Staff answer adds the parts beyond the client: the API contract (client ids, sequence numbers, sync cursors) agreed with backend, a sync module other features can reuse, the migration plan for existing users’ local data, and a rollout with a remote flag on the new sync path plus the metric that would trigger turning it off. How the Staff call is made in the debrief is in the Staff mobile system design interview.

Sources

Free guide: the Phoenix Method