← All courses

Local Persistence & Offline-First

🗓 May 31, 2026 ⏱ 3 min read

Why offline-first matters

Users expect apps to open instantly and keep working in elevators, on planes and in poor coverage. The way to achieve this is offline-first design: your app reads and writes to a local database, which is the single source of truth, and the network simply syncs that local data with the server in the background.

The single source of truth

This is the central idea. Instead of the UI talking to the network and showing whatever comes back, the UI observes the local database. The repository updates the database from the network; the database tells the UI to refresh. The user always sees something instantly (cached data), even offline.

UI Local DB (truth) Network observe sync

Choosing local storage

  • Key-value (DataStore/UserDefaults) — small settings and flags.
  • Relational DB (Room/SQLite, Core Data/SwiftData, Drift) — structured, related, queryable data. The backbone of offline-first.
  • Files — images, documents, downloaded media.
  • Secure storage (Keystore/Keychain) — tokens and secrets.

Reactive database = automatic UI updates

Modern databases expose data as an observable stream (Room Flow, SwiftData @Query). The repository writes fresh data; the UI, observing the database, updates automatically — no manual refresh. This reactive loop is the heart of a clean offline-first design.

// repository: DB is the source of truth
fun observePosts(): Flow<List<Post>> = postDao.observeAll()

suspend fun refresh() {
    val fresh = api.getPosts()      // from network
    postDao.upsertAll(fresh)        // write to DB -> UI updates itself
}

Writes while offline

Offline-first also means writing offline. The user creates/edits something; you save it locally immediately (so the UI reflects it), mark it as “pending sync”, and push it to the server when back online. This needs a sync strategy and conflict handling (next lessons).

Cache invalidation & freshness

Stored data goes stale. Decide a policy: refresh on screen open, after a time-to-live, on pull-to-refresh, or via push. Store timestamps so you know when data was last synced and whether to refetch.

Common mistakes

  • Treating the network as the source of truth, so the app is blank offline.
  • Showing a spinner on every open instead of cached data immediately.
  • No plan for offline writes or staleness, leading to lost or outdated data.
Summary: Build offline-first: make the local database the single source of truth, have the UI observe it reactively, and sync with the network in the background. Pick the right storage per data type, support offline writes with a pending-sync state, and define a freshness/invalidation policy.