Offline-First Architecture
📖 Concept
Offline-first architecture ensures your app works fully without a network connection, syncing data when connectivity returns. This is a critical differentiator at Google interviews.
Core principles:
- Local database is the source of truth — UI always reads from local storage
- Optimistic updates — User actions apply immediately to local DB, then sync
- Background sync — Use WorkManager for reliable sync when online
- Conflict resolution — Handle cases where offline edits conflict with server changes
Sync strategies:
- Full sync: Download all data every time. Simple but wasteful for large datasets.
- Delta sync: Only sync changes since last sync timestamp/token. Efficient.
- Event sourcing: Store events (created, updated, deleted) instead of state. Most flexible.
- CRDT (Conflict-free Replicated Data Types): Data structures that automatically merge without conflicts.
Conflict resolution approaches:
- Last-write-wins (LWW): Simplest. Whoever saved last overwrites. Data can be lost.
- Server-wins: Server version always takes precedence. Client re-fetches.
- Client-wins: Client version overwrites server. Risk of overwriting other users' changes.
- Manual merge: Show conflict to user, let them choose. Best for critical data.
- Three-way merge: Compare both versions with a common ancestor. Git-style.
WorkManager for reliable sync: Survives process death, respects battery constraints, exponential backoff.
💻 Code Example
1// Offline-first sync architecture23// 1. Entity with sync metadata4@Entity(tableName = "documents")5data class DocumentEntity(6 @PrimaryKey val id: String,7 val title: String,8 val content: String,9 val localVersion: Int = 1,10 val serverVersion: Int = 0,11 val syncState: String = "PENDING", // SYNCED, PENDING, CONFLICT12 val modifiedAt: Long = System.currentTimeMillis(),13 val isDeleted: Boolean = false // Soft delete for sync14)1516// 2. DAO with sync-aware queries17@Dao18interface DocumentDao {19 @Query("SELECT * FROM documents WHERE isDeleted = 0 ORDER BY modifiedAt DESC")20 fun getAll(): Flow<List<DocumentEntity>>2122 @Query("SELECT * FROM documents WHERE syncState = 'PENDING'")23 suspend fun getPendingSync(): List<DocumentEntity>2425 @Upsert26 suspend fun upsert(doc: DocumentEntity)2728 @Query("UPDATE documents SET isDeleted = 1, syncState = 'PENDING' WHERE id = :id")29 suspend fun softDelete(id: String) // Soft delete, sync later30}3132// 3. Repository with optimistic updates33class DocumentRepository @Inject constructor(34 private val dao: DocumentDao,35 private val api: DocumentApi,36 private val workManager: WorkManager37) {38 fun getDocuments(): Flow<List<Document>> =39 dao.getAll().map { it.map(DocumentEntity::toDomain) }4041 suspend fun saveDocument(doc: Document) {42 // Optimistic: save locally FIRST, UI updates instantly43 dao.upsert(doc.toEntity().copy(44 syncState = "PENDING",45 localVersion = doc.localVersion + 1,46 modifiedAt = System.currentTimeMillis()47 ))48 // Schedule background sync49 scheduleSyncWork()50 }5152 private fun scheduleSyncWork() {53 val work = OneTimeWorkRequestBuilder<SyncWorker>()54 .setConstraints(Constraints.Builder()55 .setRequiredNetworkType(NetworkType.CONNECTED)56 .build())57 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)58 .build()59 workManager.enqueueUniqueWork("doc_sync", ExistingWorkPolicy.REPLACE, work)60 }61}6263// 4. Sync Worker with conflict handling64class SyncWorker @AssistedInject constructor(65 @Assisted context: Context,66 @Assisted params: WorkerParameters,67 private val dao: DocumentDao,68 private val api: DocumentApi69) : CoroutineWorker(context, params) {7071 override suspend fun doWork(): Result {72 val pending = dao.getPendingSync()73 for (doc in pending) {74 try {75 val serverDoc = api.sync(doc.toSyncRequest())76 if (serverDoc.version > doc.localVersion) {77 // Conflict! Server has newer version78 dao.upsert(doc.copy(syncState = "CONFLICT"))79 } else {80 dao.upsert(doc.copy(81 syncState = "SYNCED",82 serverVersion = serverDoc.version83 ))84 }85 } catch (e: Exception) {86 return if (runAttemptCount < 3) Result.retry() else Result.failure()87 }88 }89 return Result.success()90 }91}
🏋️ Practice Exercise
Practice:
- Implement a complete offline-first CRUD app with sync using Room + WorkManager
- Design a conflict resolution UI that shows both versions side-by-side
- Implement delta sync using a server-provided sync token
- Test offline behavior: disable network, make changes, re-enable and verify sync
- Handle the edge case: user deletes a document offline, another user edits it online
⚠️ Common Mistakes
Treating network as the source of truth — the app breaks immediately when offline
Not using soft deletes for synced entities — hard deletes can't be synced
Ignoring conflict resolution — silently overwriting data leads to data loss
Not using WorkManager for sync — manual sync in a Service gets killed by the OS
Syncing everything instead of delta sync — wastes bandwidth and battery
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Offline-First Architecture. Login to unlock this feature.