Mobile Architecture

Offline-First Design

Designing mobile applications where local storage is the primary data source and network connectivity is an enhancement, not a requirement — covering sync strategies, conflict resolution, and background scheduling.

⏱ 12 min read

What it is

Offline-first design treats local storage as the primary data source for the application. Reads always come from the local database (Room on Android, Core Data or Realm on iOS); writes go to the local database immediately, providing instant feedback to the user. A background sync process propagates changes to the server when connectivity is available and pulls down remote changes into the local store. The UI observes the local database via reactive streams — it does not know or care whether the data came from the network or was already cached locally.

This contrasts with network-first design (also called "online-only"), where the app makes a network request for every data read and either shows an error or a loading state when offline. Offline-first eliminates the loading state for most operations, makes the app usable in intermittent or no-connectivity environments (underground, aeroplanes, rural areas), and makes the app resilient to API latency spikes.

Why it exists

Mobile networks are unreliable. Even users with good connectivity experience intermittent drops, slow handoffs between cell towers, and congested hotspots. Apps that require connectivity for every interaction frustrate users and generate support tickets. Productivity apps (note-taking, task management, document editing) must work offline by definition — users need to take notes in the subway, complete expense reports on aeroplanes, and review schedules in basements. Beyond user experience, offline-first reduces server load by batching sync operations rather than making real-time requests for every user action.

When to use

  • Productivity apps (notes, tasks, calendars) where users expect to work regardless of connectivity.
  • Field service apps used in environments with poor connectivity (warehouses, rural areas, basements).
  • Any app where load time or latency significantly impacts user experience and most data can be cached.
  • Apps with high read-to-write ratios — the cached data is read frequently, network writes are infrequent.
  • Cross-device sync apps (a user's data should be available on all their devices, synced in the background).

When not-to-use">When not to use

  • Real-time collaborative apps: Live collaboration (Google Docs simultaneous editing) requires real-time conflict resolution that goes beyond standard offline-first patterns.
  • Financial transactions: Payment processing requires server-side validation and cannot be optimistically committed locally; the offline-first pattern does not apply to irreversible financial operations.
  • Highly dynamic, personalised content: News feeds, social timelines, and recommendation engines may have data that is too dynamic to cache usefully for more than a few minutes.
  • Security-sensitive data: Data that must not persist on the device (highly classified, regulated) cannot use local storage.

Typical architecture


OFFLINE-FIRST DATA FLOW
──────────────────────────
  UI → Repository → Local DB (Room/Core Data) → emit to UI
                         ↑↓
                    Sync Worker
                         ↓
                    Remote API

  Read:  UI calls repository → returns flow from local DB → instant
  Write: UI calls repository → writes to local DB → instant feedback
         SyncWorker picks up pending local changes → sends to API
         API response updates local DB → flows emit new data to UI

LOCAL STORAGE OPTIONS
─────────────────────
  Android: Room (SQLite ORM, reactive with Flow/LiveData)
           SQLDelight (multiplatform, type-safe SQL)
           DataStore (preferences, settings)
  iOS:     Core Data (Apple ORM, NSFetchedResultsController)
           SwiftData (Swift-native, Swift 5.9+)
           Realm (third-party, reactive, cross-platform)

SYNC STRATEGIES
────────────────
  Optimistic sync: Write locally, assume success, reconcile later
    + Instant feedback
    - Must handle server rejections gracefully (rollback)

  Pessimistic sync: Write locally, mark as pending, wait for server ack
    + Server is authoritative; no rollback needed
    - Changes remain in "pending" state until connectivity restored

CONFLICT RESOLUTION
────────────────────
  LWW (Last Write Wins): Compare timestamps; newest version wins
    + Simple; works for most use cases
    - Clock skew can cause incorrect resolution

  CRDT (Conflict-free Replicated Data Type):
    Grow-only counter (G-Counter), OR-Set, LWW-Register
    + Mathematically guaranteed to converge
    - Limited data types; learning curve

  Three-way merge: Base + local change + remote change → merged
    + Can preserve intent of both changes
    - Complex; requires storing base version

BACKGROUND SYNC SCHEDULING
────────────────────────────
  Android: WorkManager (replaces JobScheduler, AlarmManager)
           Constraints: NetworkType.CONNECTED, battery not low
           Periodic work: sync every 15 minutes (minimum interval)
  iOS:     BGTaskScheduler (iOS 13+)
           BGProcessingTask (long tasks, device charging)
           BGAppRefreshTask (short periodic refresh)

Pros and cons

Pros

  • Instant UI response: reads from local DB are microseconds, not milliseconds.
  • Works in no-connectivity and intermittent-connectivity environments.
  • Reduced server load: changes are batched and synced, not sent per-interaction.
  • More resilient to API outages — the app continues to function.
  • Better battery life: background sync can be deferred to charging or low-CPU periods.

Cons

  • Significantly more complex than network-first: sync engine, conflict resolution, and schema migration all require careful design.
  • Data can be stale — users may see outdated information until next sync cycle.
  • Local storage on the device can be inspected; sensitive data requires encryption at rest.
  • Schema migrations in local DB must be backwards-compatible across app versions (users may not update immediately).
  • Sync conflicts require user-visible resolution UX in some cases — edge case but hard to get right.

Implementation notes

Sync queue pattern: Every write to the local DB also inserts a record into a sync queue table: (id, operation, entity_type, entity_id, payload, created_at, attempts). The WorkManager/BGTaskScheduler picks up pending queue items, sends them to the API in order, and marks them as synced or increments the attempt counter on failure. The sync queue is the source of truth for what needs to be sent — if the app crashes mid-sync, the queue items are still there and will be retried. Implement exponential back-off on failed attempts to avoid hammering a degraded API.

Delta sync: Rather than re-fetching all data on every sync, use a server-side cursor or timestamp. The client sends ?since=2025-01-15T12:00:00Z and the server returns only changes since that timestamp. The client stores the last sync timestamp and uses it on the next sync. This dramatically reduces bandwidth and server load for large datasets. The server must maintain a reliable updated_at timestamp on all entities and index it for efficient range queries.

Common failure modes

  • Conflict ignored: The sync engine silently overwrites local changes with the server version on every sync — the user loses offline work without warning.
  • Sync queue grows unboundedly: Offline actions accumulate in the sync queue; when connectivity is restored, a burst of requests overwhelms the API. Implement rate limiting and batching.
  • No schema migration strategy: A local DB schema change in a new app version breaks existing installs that upgrade without a carefully crafted Room/Core Data migration.
  • Stale data not indicated: Users see data that is hours old without any indication that it has not been refreshed — implement a "last synced at" indicator.

Decision checklist

  • Is the local database the primary data source, with the UI observing local data via reactive streams?
  • Is a sync queue used to track pending writes that need to reach the server?
  • Is delta sync (timestamp or cursor-based) implemented to minimise bandwidth?
  • Is a conflict resolution strategy defined (LWW, CRDT, or three-way merge) and tested?
  • Are background sync tasks scheduled with appropriate constraints (network available, battery not critical)?
  • Is local storage encrypted for sensitive data?

Example use cases

  • Field service app: Technicians complete work orders in basements without connectivity. WorkManager syncs completed orders when the van returns to the depot. LWW conflict resolution handles the rare case where a dispatcher also edits the same order.
  • Note-taking app: Notes are stored in Room immediately; CRDT-based merge ensures that notes edited on phone and tablet while both offline are merged correctly when either syncs.
  • E-commerce browsing: Product catalogue is cached in Room on first load; delta sync refreshes changed products every 30 minutes in the background. Browse is instant even offline.
  • Mobile Architecture Patterns — The repository pattern in MVVM is the integration point for the offline-first local/remote abstraction.
  • Mobile Performance — Offline-first eliminates network wait time, directly improving perceived performance.
  • Event Store — The sync queue pattern is a simplified event store applied to client-side operations.

Further reading