Implementing Conflict Resolution During Data Synchronization
Conflict arises when the same record is edited in two places before sync. User edited note on phone offline — and simultaneously on tablet. Both changes locally correct but contradict each other. Must decide: which wins, or how to merge them. No universal answer — strategy depends on data type and business logic.
Vector Clocks and Timestamp
Simplest strategy — Last Write Wins (LWW): newer timestamp wins. Obvious downside — on client clock skew, wrong version wins. Client clocks unreliable: user can change device time.
Reliable variant — server time. Client doesn't trust its clock; server assigns timestamp on write. Then LWW works correctly.
More advanced — Vector Clocks. Each client has ID, each change tracked by version vector:
data class VectorClock(
val clocks: Map<String, Long> = emptyMap()
) {
fun increment(clientId: String): VectorClock =
copy(clocks = clocks + (clientId to (clocks[clientId] ?: 0L) + 1))
fun happensBefore(other: VectorClock): Boolean =
clocks.all { (k, v) -> v <= (other.clocks[k] ?: 0L) } &&
clocks != other.clocks
fun isConcurrentWith(other: VectorClock): Boolean =
!happensBefore(other) && !other.happensBefore(this)
}
If clockA.happensBefore(clockB), version B is later, use it. If isConcurrentWith, conflict, need manual or automatic resolution.
CRDT for Automatic Merging
CRDT (Conflict-Free Replicated Data Types) — data structures safely merged without conflicts mathematically. Several types:
- G-Counter — increment only. Each device stores its counter, total is sum. Applies to view counts, likes.
- LWW-Register — Last Write Wins register via timestamp. Primitive but works for atomic values.
- OR-Set — set of elements where add and remove don't conflict.
// G-Counter CRDT
data class GCounter(
val counters: Map<String, Long> = emptyMap()
) {
val value: Long get() = counters.values.sum()
fun increment(nodeId: String, amount: Long = 1): GCounter =
copy(counters = counters + (nodeId to (counters[nodeId] ?: 0L) + amount))
fun merge(other: GCounter): GCounter =
copy(counters = (counters.keys + other.counters.keys).associateWith { key ->
maxOf(counters[key] ?: 0L, other.counters[key] ?: 0L)
})
}
For production CRDT use in mobile apps, ready libraries exist: Automerge (Rust-core, Swift and Kotlin ports) and Yjs (JavaScript, works via React Native).
Three-Way Merge
Best approach for text content — like Git. Need common base (version before divergence), changes from client A and client B.
data class DocumentVersion(
val id: String,
val baseVersion: Long, // version changes calculated from
val content: String,
val patches: List<Patch> // list of changes from base
)
class MergeStrategy {
fun merge(base: String, clientA: String, clientB: String): MergeResult {
val patchesA = diff(base, clientA)
val patchesB = diff(base, clientB)
val conflicts = findOverlappingPatches(patchesA, patchesB)
return if (conflicts.isEmpty()) {
MergeResult.AutoMerged(apply(base, patchesA + patchesB))
} else {
MergeResult.Conflict(
autoMergedContent = apply(base, nonConflictingPatches(patchesA, patchesB)),
conflicts = conflicts
)
}
}
}
On auto merge, apply both edits. On overlap, ask user to choose or edit manually.
Server-Side Conflict Logic
Client on sync sends:
{
"entityId": "note-123",
"baseVersion": 7,
"clientVersion": 9,
"changes": [...],
"clientId": "device-abc",
"timestamp": 1712345678000
}
Server checks current version. If current = baseVersion, clean merge, no conflict, apply changes. If current > baseVersion, someone changed after our base. Server returns:
{
"status": "conflict",
"serverVersion": 10,
"serverContent": "...",
"baseContent": "...",
"clientVersion": 9
}
Client gets conflict and runs 3-way merge locally.
Conflict Resolution Strategies by Data Type
| Data Type | Recommended Strategy |
|---|---|
| Notes, documents | 3-way merge, manual resolution on overlap |
| User settings | LWW with server time |
| Counters (likes, views) | G-Counter CRDT |
| Shopping cart | OR-Set CRDT (union both versions) |
| Order status | Server wins — server is authoritative |
| Map position | LWW |
Strategy choice is product, not technical: is losing user data worse than duplicates?
Storing Version History
For correct conflict resolution you need history. Minimum: store baseVersion and deltas from it. On deep merge, full history or snapshots.
@Entity(tableName = "document_versions")
data class DocumentVersionEntity(
@PrimaryKey val id: String,
val documentId: String,
val version: Long,
val content: String,
val patch: String, // JSON-diff from prev version
val authorClientId: String,
val createdAt: Long
)
History grows. Need compression strategy: save snapshot every N versions, delete intermediate after N days.
Conflict resolution implementation with 3-way merge, CRDT or LWW depending on data types: 3–6 weeks. Server part separate estimate. Cost calculated individually.







