Offline-First Architecture Quiz
DATA › Offline
In a correctly designed offline-first app, where do the UI and domain layers read data from?
- Directly from the network source, so they always get fresh data.
- From the repository, which reads the local source of truth.
- From an in-memory cache that is cleared each time the app starts.
- From whichever source returned fastest on the most recent request.
Answer: From the repository, which reads the local source of truth.
Higher layers talk only to the repository, which reads from the local store; network responses are written to local first so reads always reflect one canonical source.
Which write strategy is most appropriate for a bank transfer that must not be applied unless it reaches the server?
- Lazy (local-first) write
- Queued write drained later
- Online-only write
- Fire-and-forget local write
Answer: Online-only write
Online-only writes go to the network first and only update local on success, surfacing failures immediately, which is required for time-sensitive operations that must succeed remotely.
What does the stale-while-revalidate approach prioritize?
- Blocking the UI until fresh network data has fully arrived
- Show cached data instantly, then refresh in the background
- Never caching data, so the UI always waits on live requests
- Refreshing only when the user manually pulls down to refresh
Answer: Show cached data instantly, then refresh in the background
Stale-while-revalidate serves the cached value immediately for responsiveness, then fetches fresh data and updates the cache so observers re-emit the new state.
Why is WorkManager preferred over a plain ViewModel coroutine for background sync?
- It runs sync on the main thread, making background work feel simpler
- It survives process death and reboots, with constrained retryable work
- It guarantees sync finishes within 100 ms, even on slow or busy devices
- It removes the need for a local database, since sync state stays in memory
Answer: It survives process death and reboots, with constrained retryable work
WorkManager schedules durable, constraint-aware work that survives process death and reboot, with automatic exponential-backoff retries, which a ViewModel coroutine cannot guarantee.
A SyncWorker (CoroutineWorker) fails to reach the server due to a transient error. What should doWork() return?
- Return Result.success() to mark the sync as already complete
- Return Result.failure() to stop this sync from being retried
- Return Result.retry() so WorkManager retries with backoff
- Throw an exception so WorkManager restarts the work immediately
Answer: Return Result.retry() so WorkManager retries with backoff
Returning Result.retry() tells WorkManager to reschedule the work using its exponential-backoff policy, the correct response to a transient/recoverable failure.
What is a key limitation of a simple last-write-wins conflict resolution strategy?
- It needs a local database on every device before it can compare writes
- It can silently drop a concurrent edit, keeping only the newest write
- It cannot work with timestamps, since conflicts must be merged manually
- It always requires the user to approve each conflict before any save
Answer: It can silently drop a concurrent edit, keeping only the newest write
Last-write-wins keeps only the newest write by timestamp, so a competing concurrent change can be overwritten and lost without the user being aware.
Which WorkManager constraint ensures a sync job only runs once the device has network connectivity?
- setRequiredNetworkType(NetworkType.CONNECTED)
- setRequiresCharging(true)
- setRequiresDeviceIdle(true)
- setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
Answer: setRequiredNetworkType(NetworkType.CONNECTED)
Adding a NetworkType.CONNECTED constraint defers the work until connectivity is available; charging, idle, and expedited options control unrelated scheduling behavior.
Which scenario is the best fit for a lazy (local-first) write strategy?
- Recording analytics events that can be dropped if they never reach the server
- Submitting a one-time password that must be checked on the server first
- Editing a note that must never be lost, with instant UI and background sync
- Fetching a read-only product catalog for display, with no local changes
Answer: Editing a note that must never be lost, with instant UI and background sync
Lazy writes update the local store immediately and queue the network sync, giving instant UI feedback while protecting critical user-authored data, which suits an edit that must not be lost.
What is the role of Jetpack Paging's RemoteMediator in an offline-first paged list?
- It loads network pages into Room, and Paging serves from that cache
- It replaces Room and acts as the only source of truth for the list
- It keeps pages only in memory and drops them once they scroll offscreen
- It resolves conflicts between local edits and server writes automatically
Answer: It loads network pages into Room, and Paging serves from that cache
RemoteMediator is the pull-based bridge that loads network pages into the local database; Paging then reads from that cache, so already-loaded pages remain available offline.
For an app that is offline for long stretches and syncs relational data, which synchronization model is generally more suitable?
- Pull-based on-demand fetching only, with no upfront baseline or sync state
- Re-download the entire dataset from scratch on every app launch, every time
- Poll the server every second, even when nothing has changed since last check
- Push-based: seed a baseline, then react to change signals with versioning
Answer: Push-based: seed a baseline, then react to change signals with versioning
Long offline periods and relational data favor a push model that establishes a baseline and reacts to change signals with versioning, rather than repeatedly pulling on demand.
An exception is thrown while collecting a Room Flow. How should the repository or ViewModel handle it so the UI does not crash?
- Wrap each collector in try/catch inside every Composable that reads it
- Use catch on the Flow and emit an error UI state like LCE Error
- Convert the stream to a one-shot call with .first() so it can throw normally
- Do nothing, because Room Flows can never emit an error at runtime
Answer: Use catch on the Flow and emit an error UI state like LCE Error
The catch operator intercepts upstream exceptions and lets you emit an error UI state (commonly an LCE sealed type), keeping the stream alive instead of propagating a crash.
Why should each queued offline write carry a client-generated unique ID (such as a UUID) when it is eventually sent to the server?
- It lets Room keep pending writes ordered by when they were queued.
- It removes the need to authenticate the write before sending it.
- So the server can spot a retried write and skip applying it twice.
- Because Room cannot use auto-generated primary keys in offline queues.
Answer: So the server can spot a retried write and skip applying it twice.
A stable client-supplied ID lets the server deduplicate retried requests, so a write that succeeded but whose response was lost is not applied a second time on retry.
How do you prevent overlapping or duplicate periodic sync jobs from being enqueued every time the app starts?
- Use unique work with a stable name and ExistingWorkPolicy.KEEP
- Call enqueue() from Application.onCreate() on every app launch
- Start the sync on a new background thread each time the app opens
- Set a very large backoff delay so periodic jobs almost never run
Answer: Use unique work with a stable name and ExistingWorkPolicy.KEEP
Enqueuing unique work with a stable name and a policy like KEEP ensures WorkManager coalesces requests so only one sync chain for that name exists, avoiding duplicates.
When syncing deletions in an offline-first app, why is a soft-delete (tombstone) often used instead of immediately removing the row locally?
- It keeps the local database smaller than a hard delete would in practice
- Room's schema validation requires a deleted flag on every entity row
- It makes Flow emissions fire faster after a delete in the UI layer
- It marks the item deleted so sync can send it and prevent re-creation
Answer: It marks the item deleted so sync can send it and prevent re-creation
A tombstone preserves the intent to delete so the sync layer can push it to the server; a hard local delete would leave no record and a later pull could re-create the row.