| name | offline-first |
| description | Mobile architecture designed for offline functionality |
| category | mobile-development |
| difficulty | advanced |
| tags | ["mobile","offline","sync","local-storage"] |
| author | OpenCode Community |
| version | 1 |
| last_updated | 2024-01-15T00:00:00.000Z |
Offline-First Architecture
What I Do
I am Offline-First Architecture, a design approach that prioritizes application functionality regardless of network connectivity. I assume network availability is the exception rather than the norm, designing applications to work seamlessly offline with data stored locally. I synchronize local changes with remote servers when connectivity is restored using conflict resolution strategies. I implement various storage mechanisms including SQLite, Realm, IndexedDB, and file systems. I handle background sync, push notifications for sync completion, and merge strategies for concurrent edits. I optimize for low-bandwidth scenarios, progressive data loading, and graceful degradation. I enable users to be productive during network outages while ensuring data consistency upon reconnection.
When to Use Me
- Mobile applications with unreliable connectivity
- Field service and enterprise mobile apps
- Productivity applications requiring data access anywhere
- IoT and embedded device applications
- Applications with heavy data consumption patterns
- User-facing scenarios with variable network conditions
- Applications requiring immediate responsiveness
- Compliance scenarios requiring local data audit trails
Core Concepts
Local Storage: Primary data store that persists data locally on the device.
Sync Queue: FIFO queue managing pending changes awaiting synchronization.
Conflict Resolution: Strategies for merging concurrent local and remote edits.
Background Sync: Automatic synchronization when app is not active.
Data Versioning: Tracking data states with timestamps or version numbers.
Optimistic UI: Immediate user feedback assuming successful operations.
Delta Sync: Transferring only changed data rather than full datasets.
Connectivity Awareness: Real-time detection of network state changes.
Code Examples
Example 1: Offline Data Store with Conflict Resolution
abstract class OfflineStore<T> {
protected abstract createLocalStorage(): LocalStorage
protected abstract createSyncQueue(): SyncQueue
protected abstract createConflictResolver(): ConflictResolver<T>
async save(entity: T): Promise<void> {
const localStorage = this.createLocalStorage()
const syncQueue = this.createSyncQueue()
await localStorage.save(entity)
await syncQueue.enqueue({
type: 'UPSERT',
entity,
timestamp: Date.now(),
version: entity.version || 0
})
}
async delete(id: string): Promise<void> {
= .()
syncQueue = .()
.(id)
syncQueue.({
: ,
: id,
: .()
})
}
(: ): <T | > {
= .()
.(id)
}
(): <T[]> {
= .()
.()
}
(): <> {
= .()
syncQueue = .()
conflictResolver = .()
pendingChanges = syncQueue.()
: = { : , : , : [] }
( change pendingChanges) {
{
remoteEntity = .(change.)
(remoteEntity && change. === ) {
conflict = conflictResolver.(
change.,
remoteEntity
)
(conflict. === ) {
mergedEntity = conflict.!
.(mergedEntity)
.(mergedEntity)
} (conflict. === ) {
.(remoteEntity)
syncQueue.(change.)
}
syncResult..(conflict)
} {
.(change.)
syncQueue.(change.)
}
syncResult.++
} (error) {
syncResult.++
syncQueue.(change., error)
}
}
syncResult
}
(: ): <T | >
(: T): <>
}
{
:
: |
?: T
?:
:
:
:
: | |
}
{
:
:
: <T>[]
}
<T> {
: T
: T
: | |
?: T
}
Example 2: SQLite Database with React Native
import SQLite from 'react-native-sqlite-storage'
const DATABASE_NAME = 'offline_app.db'
interface Database {
open(): Promise<void>
close(): Promise<void>
execute(sql: string, params?: any[]): Promise<any>
transaction(fn: (tx: Transaction) => Promise<void>): Promise<void>
}
interface Transaction {
executeSql(sql: string, params?: any[]): Promise<{ rows: any[], insertId: number }>
}
class SQLiteDatabase implements Database {
private db: SQLite.SQLiteDatabase | null =
(): <> {
. = .({
: ,
: ,
: ,
})
.()
}
(): <> {
.()
.()
.()
}
(: , ?: []): <> {
(!.) ()
( {
.!.(sql, params).( {
({ : results., : results. })
}).(reject)
})
}
(: <>): <> {
(!.) ()
( {
.!.( {
({
: {
( {
tx.(sql, params, {
({ : results., : results. })
}).(reject)
})
}
})
}).(resolve).(reject)
})
}
(): <> {
(.) {
.(.)
. =
}
}
}
{
:
() {
. = db
}
(: ): <> {
..(
,
[user., user., user.]
)
}
(: ): <> {
..(
,
[user., user., user.]
)
}
(: ): <> {
..(
,
[id]
)
}
(: ): < | > {
result = ..(
,
[id]
)
result.. > ? result..() :
}
(): <[]> {
result = ..(
)
result..()
}
(): <[]> {
result = ..(
,
[]
)
result..()
}
(: ): <> {
..(
,
[, id]
)
}
}
Example 3: Network Connectivity Monitor
import { NetInfoState, NetInfoSubscription, addEventListener } from '@react-native-netinfo/ios'
type ConnectivityChangeListener = (isConnected: boolean, details: NetInfoState) => void
class ConnectivityMonitor {
private listeners: Set<ConnectivityChangeListener> = new Set()
private subscription: NetInfoSubscription | null = null
private currentState: NetInfoState | null = null
startMonitoring(): void {
this.subscription = addEventListener((state) => {
this.currentState = state
const isConnected = state.isConnected
this.listeners.forEach(listener => listener(isConnected, state))
})
}
stopMonitoring(): {
(.) {
.()
. =
}
}
(: ): {
..(listener)
..(listener)
}
(): | {
.
}
(): {
.?. ||
}
(): {
.?. ||
}
(): {
.?. ===
}
(): {
.?. ===
}
}
(): { : ; : } {
[connectivity, setConnectivity] = ({ : , : })
( {
monitor = ()
unsubscribe = monitor.( {
({
isConnected,
: state.
})
})
monitor.()
{
()
monitor.()
}
}, [])
connectivity
}
Example 4: Background Sync Worker
import { AppState } from 'react-native'
import BackgroundTask from 'react-native-background-fetch'
class BackgroundSyncManager {
private isSyncing = false
private pendingSync = false
async initialize(): Promise<void> {
const status = await BackgroundTask.configure({
minimumFetchInterval: 15,
stopOnTerminate: false,
startOnBoot: true,
requiredNetworkType: BackgroundTask.NETWORK_TYPE_ANY,
})
if (status === BackgroundTask.STATUS_AVAILABLE) {
await BackgroundTask.registerBackgroundTask(
'sync-task',
this.performSync.bind(this)
)
}
}
async performSync(): Promise<void> {
if (this.) {
. =
}
. =
{
connectivity = ()
(!connectivity.()) {
}
syncManager = ()
result = syncManager.()
.(, result)
(result. > ) {
.(result.)
}
} (error) {
.(, error)
} {
. =
(.) {
. =
.()
}
}
.()
}
(: ): <> {
delay = .( * .(, failedCount), )
( (resolve, delay))
.(
,
..()
)
}
}
{
:
:
:
(): <> {
: = { : , : , : }
userResult = .()
result. += userResult.
result. += userResult.
result. += userResult.
postResult = .()
result. += postResult.
result. += postResult.
result. += postResult.
result
}
(): <> {
pendingUsers = ..()
: = { : , : , : }
( user pendingUsers) {
{
remoteUser = .(user.)
(remoteUser) {
conflict = ..(user, remoteUser)
(conflict. === ) {
..(conflict.!)
.(conflict.!)
} (conflict. === ) {
..(remoteUser)
}
result.++
} {
.(user)
}
..(user.)
result.++
} (error) {
..(user.)
result.++
}
}
result
}
}
Example 5: Conflict Resolution Strategies
class LastWriteWinsResolver<T extends { updatedAt: number }> {
resolve(local: T, remote: T): ConflictResolution<T> {
if (local.updatedAt >= remote.updatedAt) {
return { resolution: 'LOCAL', entity: local }
} else {
return { resolution: 'REMOTE', entity: remote }
}
}
}
class FieldMergeResolver<T extends { version: number }> {
resolve(local: T, remote: T, mergeFields: (local: T, remote: T) => T): ConflictResolution<T> {
const localVersion = local.version || 0
const remoteVersion = remote.version || 0
if (localVersion > remoteVersion) {
return { resolution: 'LOCAL', entity: local }
} else if (remoteVersion > localVersion) {
{ : , : remote }
} {
merged = (local, remote)
merged. = .(localVersion, remoteVersion) +
{ : , : merged }
}
}
}
{
(: , : ): <> {
mergeFields = (: , : ): {
{
: local.,
: local. >= remote. ? local. : remote.,
: local. || remote.,
: .(local., remote.) + ,
: .()
}
}
resolver = <>()
resolver.(local, remote, mergeFields)
}
}
<T { : ; : }> {
(: T, : T, : T | ): <T> {
hasBase = base !==
baseVersion = base?. ||
localChanged = local. !== baseVersion
remoteChanged = remote. !== baseVersion
(!localChanged) {
{ : , : remote }
} (!remoteChanged) {
{ : , : local }
} (hasBase && .(local) === .(base)) {
{ : , : remote }
} (hasBase && .(remote) === .(base)) {
{ : , : local }
} {
{
: ,
: { ...remote, : .(local., remote.) + }
}
}
}
}
Best Practices
- Always assume offline first; treat connectivity as an enhancement
- Use SQLite or Realm for reliable local data persistence
- Implement optimistic UI updates for immediate user feedback
- Queue operations when offline with persistent storage
- Use version numbers or timestamps for conflict detection
- Implement exponential backoff for sync retries
- Use background fetch for periodic sync when app is idle
- Provide clear UI indicators for sync status
- Implement proper conflict resolution strategies per entity type
- Test thoroughly with simulated network conditions
Core Competencies
- Local data persistence (SQLite, Realm, AsyncStorage)
- Synchronization queue management
- Conflict resolution strategies
- Background sync and fetch
- Network connectivity detection
- Optimistic UI updates
- Delta synchronization
- Version control and data merging
- Offline-first UI patterns
- Push notification sync triggers
- Data migration and schema evolution
- Testing with network simulation
- Security considerations for offline data
- Battery optimization strategies