Articles / Technical
Offline-First Mobile Architecture Explained
In most Western development tutorials, offline support is an afterthought, a nice-to-have that gets added in a later sprint. In South Africa, it's a baseline requirement. Variable connectivity, load-shedding, and data costs mean your users will regularly be without a network connection. An app that fails completely when offline doesn't just frustrate users; it loses them.
What offline-first actually means
"Offline-first" is not the same as "works offline sometimes." An offline-first app's primary data path is local. The user never waits for a network response to see data they've already loaded. Reads are instant because they come from local storage, not from a server 200ms away.
Writes happen immediately too. When a user submits a form or takes an action, the app responds instantly by writing to local storage. The network request happens in the background, and the user doesn't wait for it.
The result is an app that feels fast always, works fully when offline, and syncs automatically when connectivity returns, without the user having to think about it.
Why it matters in the South African context
South African mobile users face network conditions that are genuinely different from the environments most mobile development resources assume. Load-shedding disconnects entire suburbs from connectivity several hours a day. Many townships and rural areas have inconsistent LTE coverage. Prepaid users run out of data mid-session.
An app designed for reliable 4G fails all of these users in all of these situations. An offline-first app treats connectivity as an optional enhancement rather than a requirement, and the experience remains consistent whether the user is on fibre WiFi or has been offline for two hours.
In South Africa, offline-first isn't a premium feature. It's respect for your users' actual network conditions.
The pattern: local database as source of truth
The core of offline-first architecture is simple: the local database is the single source of truth. All reads come from local storage. The remote backend (Firestore, a REST API, GraphQL) is the sync target, not the primary data source.
In practice this means:
- →The UI observes reactive streams from the local database (in Kotlin, these are Flow<List<T>> from Room)
- →A background sync layer fetches from Firestore and writes to Room
- →When Room is updated, the UI Flow observers emit automatically, with no manual refresh needed
- →The UI never calls Firestore directly for read operations
This pattern is sometimes called the Repository pattern or Single Source of Truth (SSOT). It means the UI is always in sync with local storage, and local storage is eventually in sync with the backend.
Implementing it with Room and Firestore
In a Kotlin Multiplatform or Android app, the implementation uses Room for local storage and Firestore for the remote backend. The key is keeping them strictly separated by layer.
The local DataSource wraps Room and exposes Flow<List<T>> for reads and suspend functions for writes. It never talks to Firestore.
The remote DataSource wraps Firestore and exposes suspend functions for fetching collections and writing documents. It never talks to Room.
The Repository coordinates between them: it observes the local DataSource for UI-facing reads, and it provides a refresh function that fetches from Firestore and writes to Room. The Flow chain from Room to UI updates automatically whenever the sync layer writes new data.
- →Room DAO → Flow<List<Entity>> for UI-bound queries
- →Firestore DataSource → suspend fun fetch() for background sync
- →Repository.observe() returns localDataSource.observe(), always reading from local storage
- →Repository.refresh() calls remoteDataSource.fetch(), then localDataSource.save()
- →UI sees updates automatically when Room is written to, with no polling
Handling writes when offline
Reading offline is the simpler half of the problem. Writing offline requires a bit more care.
The optimistic update pattern handles this well: when a user takes an action, write to Room immediately and show the result in the UI at once. Queue the Firestore write. When connectivity returns, flush the queue. If the Firestore write fails (because the data was stale or there's a conflict), reconcile and update Room with the authoritative result.
For most apps, conflict resolution is straightforward: last-write-wins with a sync timestamp. For more complex collaborative scenarios, you need an explicit conflict resolution strategy, which is uncommon in most business apps.
Firebase Firestore has built-in offline persistence that handles much of this queue-and-flush behaviour automatically. The catch is that relying entirely on Firestore's offline persistence without a local Room layer gives you less control over your data model and makes complex queries (sorting, filtering, joins) harder to express.
What it means for the user experience
The user experience of an offline-first app is qualitatively different from a standard online app. Navigation is instant, with no loading spinners while waiting for network responses. Actions feel immediate. The app works during load-shedding. It works on the taxi when data runs out.
"Last synced" indicators give users confidence that the data is current without making them responsible for managing sync. A simple "Updated 3 minutes ago" in the corner is enough.
Graceful degradation matters too: when a user tries to take an action that genuinely requires connectivity (like a payment), show a clear message rather than a spinner that times out. The app should know its connectivity state and communicate it honestly.
In summary
Offline-first is harder to build than online-only. The extra layer of local storage, the sync infrastructure, and the conflict resolution logic all take time. But for South African apps in particular, the investment pays back disproportionately. An app that works offline keeps users engaged during load-shedding, on slow data, and in poor coverage areas. In a market where these are everyday realities, offline-first is not a technical nicety; it's a product requirement.
Building something in South Africa?
Black Arrows is a Johannesburg-based mobile app and software consultancy. If you're working through a decision like the ones in this article, a short conversation is usually the fastest way to get clarity.