androidengineers.Book a session

Data Synchronization

Delta Sync vs Full Sync Trade-offs

article25 minHard

Most apps need to synchronize a local database with a server. The fundamental choice is whether to fetch everything on each sync (full sync) or only what changed (delta sync).

Full Sync

Replace the local dataset entirely every sync cycle.

class FullSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result = try {
        val articles = api.getAllArticles()  // fetch entire dataset
        db.withTransaction {
            dao.deleteAll()
            dao.insertAll(articles.map { it.toEntity() })
        }
        prefs.lastSyncTime = System.currentTimeMillis()
        Result.success()
    } catch (e: IOException) {
        Result.retry()
    }
}

Pros:

  • Simple to implement and reason about
  • No server-side sync state required
  • Correct by default — no stale items survive a sync

Cons:

  • Transfer cost scales with dataset size, not change rate
  • Battery and data usage grows with collection size
  • Large datasets can't realistically be full-synced frequently

When to use: Datasets under ~500 items, or infrequent sync (e.g., daily config sync).

Delta Sync

Fetch only records changed since the last sync, identified by a server-side cursor or timestamp.

class DeltaSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        val lastSync = prefs.lastSyncCursor  // could be a timestamp or opaque cursor

        return try {
            val response = api.getArticlesSince(cursor = lastSync)

            db.withTransaction {
                // Apply upserts and deletes from the delta
                val (deleted, upserted) = response.changes.partition { it.deleted }
                dao.deleteByIds(deleted.map { it.id })
                dao.upsertAll(upserted.map { it.toEntity() })
            }

            prefs.lastSyncCursor = response.nextCursor  // advance cursor
            Result.success()
        } catch (e: IOException) {
            Result.retry()
        }
    }
}

Pros:

  • Bandwidth proportional to change rate, not dataset size
  • Much faster for large, slowly-changing datasets
  • Suitable for large catalogs, message history

Cons:

  • Server must maintain change log or support cursor-based queries
  • Complex to implement correctly (cursor management, tombstones for deletes)
  • Bugs can accumulate — local state can drift from server over time

Hybrid Strategy

Use delta sync regularly, full sync occasionally to correct drift:

fun scheduleSyncStrategy() {
    val shouldFullSync = prefs.lastFullSyncTime?.let {
        System.currentTimeMillis() - it > TimeUnit.DAYS.toMillis(7)
    } ?: true  // no prior full sync

    if (shouldFullSync) {
        enqueue(FullSyncWorker::class)
    } else {
        enqueue(DeltaSyncWorker::class)
    }
}

Server-Side Requirements for Delta Sync

FeatureImplementation
Change logAppend-only table of all mutations with a sequence number
Soft deletesdeleted_at column instead of DELETE — so clients know to remove the item
CursorSequence number or ISO timestamp the client sends on the next request
TombstonesRecords marked as deleted so clients know to remove them locally

Example API contract:

GET /api/articles?since=1690000000000
{
  "changes": [
    { "id": "a1", "title": "Updated", "deleted": false, "updatedAt": 1690001234567 },
    { "id": "a2", "deleted": true }
  ],
  "nextCursor": "1690001234567"
}

Decision Matrix

FactorUse Full SyncUse Delta Sync
Dataset size< 500 rows> 500 rows
Change frequencyHigh (most items change)Low (few items change)
Server controlLimited (can't add change log)Full control
Implementation effortLowMedium–High
Correctness guaranteesStrongNeeds occasional full sync

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Delta Sync vs Full Sync Trade-offs | Android System Design | Android Engineers