Guide · Interview prep
Mobile System Design Interview: Offline Sync, Battery and a 45-Minute Plan
31 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- A mobile system design interview asks you to design the app on the phone, not only its servers. Strong answers cover offline use, sync, battery, weak networks, push notifications (short messages a server sends to the phone) and how updates ship.
- Design offline-first: the app reads and writes a local database on the phone, the copy every screen trusts. Sync, keeping the phone and server copies in step, happens later.
- Make every retry safe. Give each queued change an idempotency key, a unique id the server uses to ignore repeats. In our run, retries without one saved 62 orders for 50 taken.
- The phone, not your app, decides when background code runs. Android's Doze mode (a deep sleep for idle phones) and Apple's background task scheduler delay it. Push services are best-effort too, so a push is a hint to sync, not a guarantee.
- Beginners can stop after the sync section. Engineers then get a runnable bug where a wrong phone clock reopens a cancelled order. They also get an Android vs iOS limits table checked as of 28 October 2026, push and rollout rules and a minute-by-minute 45-minute answer.
What goes wrong when you answer a mobile design question like a backend one?
You spend the round on servers and run out of time before the phone appears. In a mobile system design interview, the phone is the main system. It loses signal, sleeps to save battery and keeps running old versions of your app.
Picture a common setup. The interviewer asks you to design the order-taking app that field sales reps use all day in kirana shops, the small neighbourhood grocery stores. The prompt is made up, but every platform limit below comes from official Android, Firebase and Apple documentation.
A rep walks into a shop with one bar of signal, takes an order and walks out. That order must never be lost and never be saved twice. Meanwhile the back office may cancel or reprice it while the phone is offline.
A load balancer (a box that spreads requests across servers) and a bigger database answer none of that. This guide gives you the mobile-specific parts to cover, shows each one on the order app, and ends with a 45-minute plan you can rehearse.
Which words do you need before a mobile design round?
You need eight words. Each one comes back in the order app.
- Offline-first: the app does its core work without internet and catches up later. Android's architecture guide says it performs all, or a critical subset, of its core functionality offline.
- Local database: storage inside the app on the phone, often SQLite, a small database engine that runs in the app's own process.
- Sync: keeping the phone's copy of the data and the server's copy in step, in both directions.
- Outbox: a table of changes waiting to be sent. Each row stays until the server confirms it.
- Idempotency key: a unique id attached to one change, so the server can spot a repeat and ignore it. We call it an op id below.
- Conflict: two copies changed the same record in different ways before they synced.
- Push notification: a short message your server sends through Firebase Cloud Messaging (FCM) for Android or the Apple Push Notification service (APNs) to wake or alert the app.
- Staged rollout: releasing an app update to a percentage of users first, then widening it.
What is a mobile system design interview and how is it different?
It is a design round where the client app is the system. You design its local storage, sync, background work, network handling and release path, plus just enough backend to support them.
The server still matters, but the phone sets the hard limits. A backend answer assumes a reliable network, machines that run when told, and deploys you can roll back. A phone gives you none of those.
| Axis | Backend-style answer assumes | Mobile answer must handle |
|---|---|---|
| Network | A fast, stable data-centre network | Dropped and slow links, Wi-Fi login pages and metered (pay-per-use) data that change at any time |
| Source of truth | The server database | A local database the screens trust, with the server as the shared truth |
| When code runs | Whenever you schedule it | When Android or iOS allows it, based on battery, usage and idle state |
| Notifications | A queue delivers each message | Push services that may delay, merge or drop messages |
| Shipping a fix | Redeploy or roll back in minutes | Store releases rolled out over days; installed versions stay until users update |
| Resources | Add machines | Battery, storage and download size on low-end phones |
If you have read the SevyDevy guide on the frontend system design interview, this is its closest cousin. The big shift is that a web page usually loads the latest deployed code on its next visit, while an installed app does not.
How do you design offline-first local storage on the phone?
Make the local database the only thing your screens read, write every user change there first, and let a separate sync engine talk to the server. Android's offline-first guide states this rule directly.
It calls the local data source the canonical source of truth for the app. Screens observe the local database, so they update the moment a change lands, whether it came from the rep or from sync.
The naive design calls the API on every tap and shows a spinner. It is shown here because it is wrong for this app. With one bar of signal the rep waits, and with no signal the order is lost.
Choose a write strategy per action
The same Android guide names three ways to write data. Pick one per action, not one for the whole app.
- Online-only writes: send to the server, and update the local copy only if that succeeds. Fine for actions that make no sense offline, such as changing a password.
- Queued writes: put the change in a queue and drain it when the app is back online, waiting longer after each failure. Android often gives this to WorkManager, its library for background work that must finish.
- Lazy writes: write to the local database first, then queue the change for the network. The guide warns this is nontrivial, because conflicts appear when the app reconnects.
Taking an order is a lazy write. The rep sees the order saved instantly, and the outbox carries it to the server later. Conflicts get their own section below.
Decide what lives on the phone
Store what the rep needs during a day without signal, and fetch the rest on demand. Here are made-up but realistic sizes, computed with our sizing.py script.
- Product catalogue: 5,000 products at about 1 KB each is 5 MB. Keep it all on the phone; the server owns it.
- Product photos: 5,000 thumbnails at 20 KB each is 100 MB. Too big to preload on mobile data, so cache them as they are viewed.
- Shops on the rep's route: 400 shops at 2 KB is 0.8 MB. Keep them all.
- Orders: 40 orders a day at 2 KB is 80 KB of uploads. Keep recent ones; archive old ones on the server.
Offline-first order app: screens never call the server
Drag to exploreNotice that every arrow to the server starts at the sync worker. A dead network only makes the outbox wait; the rep can keep taking orders.
Diagram connections:
- Order screens to Local database (Save order, watch rows)
- Local database to Outbox table (Same transaction adds op)
- Outbox table to Sync worker (Drained when conditions allow)
- Sync worker to Sync API (Send ops, pull changes)
- Sync API to Server database (Apply once, log change)
- Sync API to FCM or APNs (Send a sync hint)
- FCM or APNs to Sync worker (Wake the app)
- Sync worker to Local database (Write pulled changes)
- Order screens: Read and write only the local database
- Local database: SQLite on the phone; source of truth for screens
- Outbox table: Changes waiting, each with an op id
- Sync worker: WorkManager job or iOS background task
- Sync API: Applies each op once, returns changes
- Server database: Shared truth plus an ordered change log
- FCM or APNs: Push service that can wake the app
How does sync work when the network keeps dropping?
Sync has two halves. Push the outbox up with retries that are safe to repeat, and pull down only what changed since the last good sync. Both halves must survive a request that dies midway.
Sending changes with an outbox and op ids
When the rep saves an order, one local transaction writes the order row and an outbox row. A transaction is a group of writes that all succeed or all fail. The outbox row carries an op id, a random id such as a UUID (a 128-bit identifier that is practically never repeated), created once before the first send.
The sync worker sends rows in order and deletes each one only after the server replies. If the reply never arrives, the phone cannot tell whether the server saved the order or never saw it.
Pause and predict: one send in five loses its request, one reply in five is lost, and the phone retries both. Without op ids, how many of 50 orders get saved more than once?
import random
class Server:
def __init__(self):
self.orders = {} # order id: order
self.seen_ops = {} # op id: the reply we already sent
def create_order(self, op_id, order, dedupe):
if dedupe and op_id in self.seen_ops:
return self.seen_ops[op_id] # a retry: same reply, no new row
order_id = f"ORD-{len(self.orders) + 1}"
self.orders[order_id] = order
self.seen_ops[op_id] = {"order_id": order_id}
return self.seen_ops[op_id]
def flaky_send(server, op, rng, dedupe):
if rng.randrange(5) == 0:
raise TimeoutError("request lost") # server never saw it
reply = server.create_order(op["op_id"], op["order"], dedupe)
if rng.randrange(5) == 0:
raise TimeoutError("reply lost") # server saved it, phone never heard
return reply
def drain(outbox, server, rng, dedupe):
sends = 0
while outbox:
sends += 1
try:
flaky_send(server, outbox[0], rng, dedupe)
outbox.pop(0) # delete only after a reply
except TimeoutError:
pass # a real app waits and backs off here
return sends
for dedupe in (False, True):
rng = random.Random(7)
server = Server()
outbox = [{"op_id": f"op-{i}", "order": {"shop": i}} for i in range(50)]
sends = drain(outbox, server, rng, dedupe)
print(f"dedupe={dedupe}: 50 orders taken, {len(server.orders)} saved, {sends} sends")It prints that without op ids the server saved 62 orders for 50 taken, after 82 sends. With op ids it saved exactly 50, from the same 82 sends.
What just happened? A lost reply looks exactly like a lost request from the phone, so retrying is the only safe move. The server must make repeats harmless by remembering op ids it has already applied.
We reran it over 1,000 random seeds. Without op ids, every run duplicated orders, between 2 and 26 per run. With op ids, no run duplicated any. Save the code as outbox_sim.py and run python3 outbox_sim.py; we used Python 3.13.
A real server stores the op id in the same database transaction as the order. It keeps op ids for longer than any client would keep retrying.
Pulling changes with a cursor
For the way down, the server keeps a change log, an ordered list of changes with a rising sequence number. The phone saves the last number it applied, called a cursor.
Each sync asks for changes after the cursor, then applies them and saves the new cursor in one local transaction. If the app dies midway, the old cursor is still there and the pull repeats.
Applying the same change twice must give the same row, so use upserts, which insert a row or overwrite it by id. In our made-up sizing, a day's 200 price and stock changes are 60 KB, about 83 times smaller than the 5 MB catalogue.
Retries should wait longer each time. Exponential backoff multiplies the wait after each failure, often doubling it. WorkManager's default backoff is exponential with a 30-second starting delay, and the delay cannot be set below 10 seconds, as of 28 October 2026.
How do you resolve sync conflicts on mobile?
Choose a rule per kind of data, not one rule for the whole app. Last write wins is fine for a rep's own notes, but orders need version checks, because the back office edits them too.
Android's offline-first guide calls last write wins a common approach for mobile apps. Devices attach a timestamp to each write, and the server discards anything older than what it holds.
The weak point is whose clock made the timestamp. A phone's clock can be wrong, because users can set it by hand and clocks drift. Last write wins then trusts a number the server cannot check. Stamping writes with the server's clock on arrival does not help here, because the stale offline edit still arrives last.
Try it on the order app. The office cancels an order at 11:00 by the server clock. A rep had edited the same order offline at 10:58, but the phone clock runs 5 minutes fast, so the edit says 11:03.
from datetime import datetime, timedelta
T = datetime(2026, 10, 28, 11, 0) # true time when the office cancels
row = {"status": "open", "qty": 10, "ts": T - timedelta(hours=1), "version": 1}
# The office cancels at 11:00. It had read version 1.
cancel = {"status": "cancelled", "qty": 10, "ts": T, "base_version": 1}
# The rep edited offline at 10:58 true time, also starting from version 1.
# The phone clock runs 5 minutes fast, so the edit is stamped 11:03.
edit = {"status": "open", "qty": 12, "ts": T + timedelta(minutes=3), "base_version": 1}
def last_write_wins(row, change):
return max(row, change, key=lambda r: r["ts"]) # newer timestamp wins
def version_check(row, change):
if change["base_version"] != row["version"]:
return row, "rejected: send the latest row back to the phone"
return {**change, "version": row["version"] + 1}, "applied"
lww = last_write_wins(last_write_wins(row, cancel), edit)
print("last write wins:", lww["status"], "qty", lww["qty"])
v, _ = version_check(row, cancel)
v, result = version_check(v, edit)
print("version check: ", v["status"], "qty", v["qty"], "|", result)It prints open qty 12 for last write wins: an old, offline edit has reopened a cancelled order, which could now ship. The version check keeps the order cancelled and rejects the stale edit.
The version check is optimistic concurrency. Each row has a version number, each change says which version it started from, and the server rejects a change whose starting version is out of date.
A version check rejects whichever change arrives second with an old version. If the rep's edit lands first, the cancel is rejected, and the office must re-read and cancel again. A state rule that cancelled orders never reopen then blocks any later edit.
The phone then pulls the latest row, tells the rep the office cancelled this order, and keeps their edit as a draft. No timestamp from the phone is trusted.
| Data | Who edits it | Rule | Why |
|---|---|---|---|
| Product catalogue and prices | Back office only | Server always wins | The phone has a read-only copy, so there is nothing to merge |
| Orders | Rep and back office | Version check plus state rules | A cancelled order must stay cancelled whatever the phone sends |
| Rep's private shop notes | One rep | Last write wins | One writer, low cost if an edit is lost |
| Shop contact details | Several reps | Merge per field, flag clashes | Two reps rarely change the same field at once |
Text that many people edit at once needs merge-friendly structures, such as CRDTs, data types designed so any two copies can merge without a referee. SevyDevy's guides on collaborative editing and file sync system design go deeper.
Which Android and iOS background limits apply as of 2026?
On both platforms the operating system decides when background code runs. You ask for work with conditions, and the system runs it when battery, usage and idle state allow.
Android limits: Doze, standby buckets and WorkManager
Doze applies on Android 6.0 and higher. When a phone sits unplugged, still and with its screen off for a while, Doze defers background CPU and network work. It suspends network access and ignores wake locks, which are requests to keep the CPU awake.
During Doze, jobs from JobScheduler, Android's system job scheduler, do not run. WorkManager uses JobScheduler internally, so its tasks wait too. Short maintenance windows let deferred work run, and they come less often over time.
App Standby Buckets, from Android 9, place each app in one of five buckets based on usage patterns: active, working set, frequent, rare or restricted. Less-used buckets get stricter limits on jobs and alarms.
The restricted bucket, added in Android 12, allows jobs once per day in a 10-minute batched session. The limits for the other four buckets are lifted while the phone charges.
For apps that target Android 8.0 or higher, moving to the background starts a window of several minutes to use services. After that the system stops the app's background services, and the docs point to scheduled jobs instead.
WorkManager is the recommended tool for work that must survive. It stores scheduled work in its own SQLite database, so work persists across app restarts and device reboots.
You can attach constraints: a network type such as UNMETERED, battery not low, charging, device idle and storage not low. All must hold before the work runs, and periodic work repeats no more often than every 15 minutes.
Two WorkManager types appear in the table below. Expedited work should start right away; in the background it runs on a time quota set by the app's standby bucket, with no quota while the app is on screen.
Long-running work can run longer than 10 minutes. WorkManager runs it as a foreground service, background work the user can see, with an ongoing notification.
iOS: BGTaskScheduler, background pushes and transfers
BGTaskScheduler, available from iOS 13, schedules background tasks. A refresh task, BGAppRefreshTask, is for small updates: the system picks the time and gives up to 30 seconds of runtime.
A processing task, BGProcessingTask, is for deferrable heavy work that can take minutes, such as database clean-up. It can require external power and network connectivity, and the system decides when to launch it.
Apple's development guide warns that the delay between scheduling a task and its launch can be many hours. Apple does not publish how often refresh tasks run; that is undocumented as of 28 October 2026.
To finish something the user started, such as sending an order, an app calls beginBackgroundTask and gets limited extra time. For big uploads, a background URLSession, Apple's networking API in background mode, hands the transfer to a separate system process.
From iOS 26, a longer job the user starts, such as exporting a day's orders, can use BGContinuedProcessingTask. It starts in the foreground, shows progress in a system display called a Live Activity, and the user can cancel it.
| Need | Android | iOS | Limit to remember |
|---|---|---|---|
| Deferrable sync with conditions | WorkManager with constraints | BGProcessingTask needing power or network | The system picks the time on both |
| Short regular refresh | Periodic WorkManager work | BGAppRefreshTask | Android: 15-minute minimum interval. iOS: up to 30 seconds per run |
| Finish what the user started | Expedited WorkManager work | beginBackgroundTask; from iOS 26, BGContinuedProcessingTask for longer jobs | Both are limited by time or quota |
| Large upload or download | Long-running WorkManager work | Background URLSession | Android work may run past 10 minutes. iOS transfers run outside your app |
| React to a server change | FCM message | Background push at priority 5 | Android: high priority with no visible notification may be lowered. iOS: not guaranteed; Apple advises two or three an hour at most |
Picking a background mechanism
Drag to exploreNotice that only the first branch is immediate. Every other path hands timing to the operating system, so the sync must be correct whenever it finally runs.
Diagram connections:
- New background need to Must the user know now? (Start here)
- Must the user know now? to Visible push (Yes)
- Must the user know now? to Did the user just start it? (No)
- Did the user just start it? to Finish-up work (Yes)
- Did the user just start it? to Is it a large transfer? (No)
- Is it a large transfer? to Background transfer (Yes)
- Is it a large transfer? to Deferred job (No)
- New background need: Something must happen off screen
- Must the user know now?: An alert the rep must see
- Visible push: FCM or APNs alert from the server
- Did the user just start it?: For example, tapping Save
- Finish-up work: Expedited work or beginBackgroundTask
- Is it a large transfer?: Photos, catalogue images
- Background transfer: Long-running work or background URLSession
- Deferred job: WorkManager or BGProcessingTask
How to check your sync survives Doze and task expiry
Queue an order offline
Turn on aeroplane mode, save an order and confirm exactly one outbox row exists, using your debug screen or a database inspector.
Force Doze on Android
Using adb, Android's command-line debug tool, run adb shell dumpsys deviceidle force-idle, then turn networking back on. The sync job should wait, because Doze suspends jobs and network access.
Leave Doze and watch it drain
Run adb shell dumpsys deviceidle unforce, then adb shell dumpsys battery reset. The outbox row should drain, and the server should show one order, not two.
Launch the task on iOS
On a real device, pause in the debugger after submit and run e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"] with your task id.
Force an early expiry
While the task runs, use the matching _simulateExpirationForTaskWithIdentifier command. The outbox row must still be there afterwards. Apple says these private calls are for development only.
How should the app handle slow, metered and flaky networks?
Assume every request can fail, stall or cost the user money. Check what kind of network you are on, keep payloads small, and let non-urgent transfers wait for a better connection.
A connected network is not always a working one. On Android, NET_CAPABILITY_VALIDATED means the system probed the network and reached the internet. A captive portal, such as a Wi-Fi login page, fails that check.
Android calls a network metered when heavy use costs the user money, data allowance or battery. Its docs warn you cannot assume Wi-Fi is unmetered, so check NET_CAPABILITY_NOT_METERED instead.
The default network can change at any time, so register a network callback rather than checking once at startup.
On iOS 13 and later, users can turn on Low Data Mode, a setting that asks apps to use less data. For work the user did not ask for right now, Apple advises these URLSession settings.
Set allowsConstrainedNetworkAccess and allowsExpensiveNetworkAccess to false and waitsForConnectivity to true. The task then waits for a suitable network instead of failing. One catch: waitsForConnectivity only helps while a connection is being set up. A connection that drops midway still returns an error.
- Send small, paged responses: changes since the cursor, not whole tables.
- Upload order photos only on unmetered networks, with WorkManager's UNMETERED constraint or an iOS background session marked discretionary.
- Set request timeouts and retry with backoff, so a stalled request does not hold the outbox forever.
- Request images sized for the screen, not the original upload.
How do push notifications fit into a mobile design?
Push is a doorbell, not a delivery van. Use it to alert the user or to tell the app something changed, then let the app fetch the details through normal sync.
The doorbell tells you someone came, but you still open the door, which is the sync pull, to collect the parcel. Where the picture breaks: APNs may keep only the latest push for an offline phone and drop older ones.
The app registers with FCM or APNs and gets a token, an address for that one install. FCM has called it a registration token; as of 28 October 2026 it is moving to a Firebase Installation ID and marks the token APIs deprecated. APNs calls it a device token. It sends the token to your server, which later sends messages through the push service. The SevyDevy notification system design guide covers the server side.
Limits that shape the payload and timing
Each limit below was read on the Firebase and Apple pages as of 28 October 2026.
- Size: FCM data messages can carry at most 4 KB. APNs payloads are limited to 4 KB (4,096 bytes), or 5 KB for internet calls. Send an order id, not the order.
- Storage while offline: APNs stores only one notification per app for an offline device, usually the latest. FCM stores up to 100 non-collapsible messages for up to 28 days. Past 100, it discards all stored messages and later tells the app, which should then do a full sync.
- Priority on iOS: priority 10 sends immediately, 5 respects power, and 1 avoids waking the device. Background pushes must use 5; using 10 is an error.
- Background pushes on iOS are low priority, are not guaranteed and may be throttled. Apple says not to send more than two or three per hour, and a force-quit app drops the held push.
- Priority on Android: high priority can wake a sleeping phone. If high-priority messages do not lead to visible notifications, FCM may lower them to normal priority, judging by the last 7 days.
FCM is also changing how urgency is set. As of 28 October 2026, its docs recommend AndroidConfigV2, a newer request format where the message type sets urgency: RemoteNotification for visible alerts and BackgroundSync for non-urgent updates.
Tokens go stale. Store each one with a last-seen time, and delete it when FCM answers UNREGISTERED, an HTTP 404. FCM treats an Android registration as expired after 270 days of inactivity, as of 28 October 2026.
A push is a hint; the cursor decides what is new
Drag to exploreNotice that the push carries no order data. If it is late or dropped, the next app open pulls the same changes, because the cursor decides what is new.
Diagram connections:
- Office cancels an order to Change log entry (Write change)
- Change log entry to Push with ids only (Notify rep's devices)
- Push with ids only to Push held or delivered (Best-effort delivery)
- Push held or delivered to Pull since cursor (Wake and sync)
- App opened later to Pull since cursor (Fallback sync)
- Pull since cursor to Local database updated (Apply and save cursor)
- Office cancels an order: Back office → sync API
- Change log entry: Sync API → server change log
- Push with ids only: Server → FCM or APNs
- Push held or delivered: Push service → phone, timing not promised
- App opened later: Rep → app, if the push was lost
- Pull since cursor: Phone → sync API
- Local database updated: Sync worker → local database
Why do app size and release rollouts shape a mobile design?
Because you cannot hot-fix an installed app the way you redeploy a server. Every version you ship stays on some phones, and a bigger download can cost installs on low-end phones and small data plans.
App size
Android's size guide says users often avoid apps that seem too large, especially in emerging markets with spotty 2G and 3G networks or data limits. It adds that size can hurt install success and increase uninstalls.
Google Play Console Help lists these limits as of 28 October 2026, by compressed download size. The base module and each feature module, a separately packaged part of the app, can be 500 MB. All modules plus the assets delivered at install time can total 4 GB.
Above 200 MB, users on mobile data see a non-blocking warning dialog. Apps still uploaded as APKs, the older single-file format, are capped at 100 MB.
On Apple platforms, as of 28 October 2026, an iOS app's total uncompressed size must be under 4 GB. The executable's code sections are capped at 500 MB for iOS 9 and later.
For the order app, ship an Android App Bundle so Play builds a smaller download per device. Turn on R8, Android's build step that strips unused code, plus resource shrinking, and download product photos on demand instead of bundling them.
Releases and rollouts
Google Play staged rollouts send an update to a percentage of users, chosen at random, and you raise the percentage yourself. Halting a rollout stops new users getting that version. Staged rollouts work only for updates, not first releases.
As of 28 October 2026, Apple's phased release spreads an update over 7 days to users with automatic updates on: 1%, 2%, 5%, 10%, 20%, 50%, then 100%. Anyone can still download it manually, and you can pause for up to 30 days in total.
Halting a Play rollout stops new users getting that version. Pausing an Apple phased release only stops automatic updates, and anyone can still download it by hand. Neither removes the version from phones that already updated, so the design needs three safety nets.
- Backward-compatible APIs: old app versions keep calling your server, so add fields instead of changing them, and version breaking changes.
- Remote feature flags: server-side switches that turn a broken feature off without a release. The SevyDevy feature flag system design guide covers how they work.
- A minimum supported version: the server tells old apps to update. On Android, Play's in-app updates offer an immediate flow that blocks use until the user updates and restarts.
How do you present a mobile system design interview answer in 45 minutes?
Spend the first ten minutes on requirements and the phone's limits, the middle twenty on client architecture and sync, and the last fifteen on battery, push, release and trade-offs. Let the phone's limits drive each choice.
This plan assumes a 45-minute round. If yours runs 60 minutes, stretch the sync deep dive first. Rounds and expectations vary by company, so ask your recruiter what the round covers.
Minutes 0 to 5: clarify
Ask who uses the app, on Android, iOS or both, and on what phones. Ask which actions must work offline and how fresh data must be.
Minutes 5 to 10: size the phone's data
Estimate what lives on the phone and what syncs daily. Say the numbers out loud, with units.
Minutes 10 to 20: draw the client
Screens, local database, outbox, sync worker, sync API and push service. Keep the backend to one or two boxes unless asked.
Minutes 20 to 30: go deep on sync
Explain op ids, the cursor, upserts and one conflict rule per kind of data. Show how a wrong phone clock breaks last write wins.
Minutes 30 to 37: battery, background and push
Name the platform tool for each background need and its limit. Make clear that push is a hint and sync is the source of correctness.
Minutes 37 to 42: network, size and release
Cover metered and captive networks, payload sizes, app size, staged rollouts, feature flags and a minimum supported version.
Minutes 42 to 45: trade-offs and metrics
Name what you would measure, such as oldest outbox row age, sync failure rate and crash-free sessions. Say what you would build next.
Worked example: the field sales order app
| Minutes | What you say | What you draw |
|---|---|---|
| 0 to 5 | Reps work all day with weak signal, mostly on low-end Android phones. Taking an order must work offline. Prices may change daily. | A requirements list with offline actions marked |
| 5 to 10 | Catalogue 5 MB stays on the phone. Photos total 100 MB, so cache on demand. Orders upload about 80 KB a day. | A small table of data, size and where it lives |
| 10 to 20 | Screens read only the local database. Saving an order writes the row and an outbox entry together. | The offline-first diagram from this guide |
| 20 to 30 | Op ids stop duplicate orders on retry. A cursor pulls about 60 KB of daily changes. Orders use version checks plus a rule that cancelled orders never reopen, so a stale edit cannot undo a cancel. | The outbox and cursor loops, plus a conflict rules table |
| 30 to 37 | Sync runs as WorkManager work with a network constraint. A push with order ids hints at changes; app open is the fallback. | The push-as-hint flow |
| 37 to 42 | Photos upload on unmetered networks only. Ship an app bundle, use staged rollouts, flags and a minimum version. | Release pipeline with a rollout percentage |
| 42 to 45 | Watch oldest outbox age and duplicate order rate. Next, I would add per-field merge for shop details. | Three metrics beside the diagram |
What just happened? Every decision in the table traces back to a phone limit: weak signal, a sleeping CPU, best-effort push or a one-way release. That chain is what turns a backend answer into a mobile one.
Questions people ask
- What is asked in a mobile system design interview?
- You are asked to design an app as it runs on the phone, plus the backend it needs. Expect questions on local storage, offline use, sync and conflicts, background work and battery, weak networks, push notifications, app size and how updates roll out. A good answer starts from these phone limits and lets them drive each choice, rather than spending the round on server scaling.
- How is a mobile system design interview different from a frontend one?
- Both focus on the client, but a phone app is installed and long-lived. A web page usually loads the latest deployed code on its next visit, while an app version stays until the user updates. Phones also restrict background work to save battery, and they lose signal more often. So mobile answers spend more time on offline storage, sync, background limits and release safety.
- Do I need to know both Android and iOS for a mobile design round?
- Know the ideas on both and the tool names on your main platform. Offline-first storage, outboxes, cursors and conflict rules are the same everywhere. For background work, name WorkManager on Android and BGTaskScheduler on iOS, and mention that the system decides when either runs. The trade-off matters more than the exact API name, but correct names build trust.
- Is last write wins good enough for offline sync?
- Only for data with one writer, where losing an edit costs little. Last write wins trusts a timestamp, and a phone's clock can be wrong. In our run, a phone clock 5 minutes fast let an old offline edit reopen a cancelled order. For shared data such as orders, use version numbers and business rules, so the server rejects stale changes.
- How often can a background sync run on Android and iOS?
- Less often than you would like, and not on an exact schedule. On Android, periodic WorkManager work repeats at most every 15 minutes, and Doze and App Standby can delay it further. On iOS, the system picks when refresh tasks run and gives each up to 30 seconds. Apple warns the delay can be many hours, as of 28 October 2026.
- Are push notifications guaranteed to arrive?
- No. As of 28 October 2026, APNs is a best-effort service and stores only one notification per app for an offline device. Apple does not guarantee background push delivery and may throttle it. FCM can lower the priority of high-priority messages that show no notification. Treat a push as a hint to sync, and make sure opening the app or a periodic job pulls the same changes.
Sources and further reading
- Android Developers: Build an offline-first app (source of truth, write strategies, last write wins)
- Android Developers: Persistent work with WorkManager
- Android Developers: Define work requests (constraints, 15-minute minimum, backoff)
- Android Developers: Optimize for Doze and App Standby
- Android Developers: App Standby Buckets
- Android Developers: Background execution limits in Android 8.0
- Android Developers: Read network state (validated and metered networks)
- Android Developers: Reduce your app size
- Android Developers: In-app updates
- Play Console Help: Optimize your app's size and stay within Google Play app size limits
- Play Console Help: Release app updates with staged rollouts
- Firebase docs: About FCM messages (payload, collapsing, time to live)
- Firebase docs: Setting the priority of a message on Android (AndroidConfigV2)
- Firebase docs: Best practices for FCM registration management
- Apple Developer: Choosing Background Strategies for Your App
- Apple Developer: BGTaskScheduler
- Apple Developer: BGProcessingTaskRequest
- Apple Developer: BGContinuedProcessingTask (iOS 26 and later)
- Android Developers: Support for long-running workers
- Apple Developer: Starting and Terminating Tasks During Development
- Apple Developer: Pushing background updates to your App
- Apple Developer: Sending notification requests to APNs
- Apple Developer: Downloading files in the background
- Apple Developer: URLSessionConfiguration allowsConstrainedNetworkAccess (Low Data Mode)
- Apple Developer: URLSessionConfiguration waitsForConnectivity
- App Store Connect Help: Release a version update in phases
- App Store Connect Help: Maximum build file sizes