What Is a Cross-Device Sync Service?
A cross-device sync service propagates state changes made on one device to all other devices owned by the same user, resolving conflicts when concurrent edits occur offline. Notes apps, password managers, and browser bookmark sync are common examples. The fundamental challenge is reconciling divergent histories without a central sequencer, which requires a versioning scheme that captures causality rather than just timestamps.
Requirements
Functional Requirements
- Sync data records (notes, settings, files) across a user devices in near real time
- Support concurrent edits on multiple devices while offline
- Detect and resolve merge conflicts using configurable strategies
- Present unresolvable conflicts to the user for manual resolution
- Provide a conflict history log per record
Non-Functional Requirements
- Convergence: all devices must eventually reach the same state
- Idempotent sync: replaying the same change must not cause duplicates
- Low sync latency when online (under 2 seconds end-to-end)
- Correct behavior during network partitions of arbitrary duration
Data Model
- records: record_id, user_id, type, payload (JSON), vector_clock (JSON), server_version (lamport counter), is_deleted, created_at, updated_at
- device_registry: device_id, user_id, device_name, last_sync_at, vector_clock_snapshot (JSON)
- sync_log: entry_id, record_id, device_id, operation (upsert, delete), vector_clock, payload_hash, synced_at
- conflicts: conflict_id, record_id, device_a_id, device_b_id, device_a_payload, device_b_payload, strategy_applied, resolved_payload, resolved_by, created_at, resolved_at
The vector_clock column is a JSON map of {device_id: counter}. Each device increments its own entry on every local write. This encodes the causal history of each record version.
Core Algorithms
Vector Clock Versioning
On local write on device D: increment vc[D] by 1. When sending to the server, include the full vector clock. The server compares the incoming vc with the stored vc using the happens-before relation: vc_a happens-before vc_b if for all devices d, vc_a[d] <= vc_b[d] and at least one is strictly less. If neither clock dominates the other, the versions are concurrent and a conflict exists.
Merge Strategies
Last-Write-Wins (LWW): when a conflict is detected, pick the version with the higher wall-clock timestamp as the winner. Simple and low-overhead but risks discarding data on fast concurrent edits. Suitable for settings and preferences where the latest intent is what matters.
Three-Way Merge: find the common ancestor (the version both conflicting clocks descend from, identified by the highest common vector clock), then apply a structural diff of ancestor-to-A and ancestor-to-B. Merge non-overlapping field changes automatically; flag overlapping field changes as conflicts. This is suitable for structured documents and is the strategy used by git-style merges.
CRDT-based merge: model data structures as Conflict-free Replicated Data Types (e.g., OR-Set for collections, LWW-Register for scalars) so all concurrent operations commute and merge automatically without conflict detection.
Conflict UI
When automatic resolution fails or LWW is not acceptable, write a conflicts row with both payloads and notify the client. The client presents a diff view with accept/reject controls. On resolution, the winning payload and resolved_by are recorded and synced to all devices as a new authoritative version.
API Design
- GET /sync?since_version=&device_id= — pull records changed since device last sync vector clock
- POST /sync — push batch of local changes; body: [{record_id, payload, vector_clock}]
- GET /conflicts — list unresolved conflicts for the user
- POST /conflicts/{id}/resolve — submit manual resolution with chosen payload
- DELETE /records/{id} — soft-delete with tombstone propagation
- POST /devices — register a new device and receive initial vector clock
Scalability Considerations
The sync pull query (WHERE user_id = ? AND server_version > ?) must be fast. Add an index on (user_id, server_version). Use a Lamport counter per user stored in Redis (INCR) to assign monotonically increasing server_version on every write, enabling efficient range queries without vector clock comparisons on the read path.
For high-frequency sync (e.g., real-time collaborative editors), use WebSocket or SSE to push change notifications to online devices, triggering an incremental pull rather than polling. This reduces both latency and unnecessary polling load.
Conflict detection is O(number of devices) per write. For users with many devices, limit the vector clock to active devices (those synced in the last 30 days) and prune stale entries to keep clocks compact.
Summary
A cross-device sync service uses vector clocks to capture causality between device writes, detects concurrent edits as conflicts when neither clock dominates, and resolves them via LWW, three-way merge, or CRDT strategies depending on data type. A Lamport server version enables efficient incremental pull queries, and WebSocket push minimizes sync latency for online devices.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do vector clocks enable versioning in cross-device sync?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each device maintains a vector clock: a map of {device_id → logical_timestamp}. On every write, the writing device increments its own component. When syncing, compare vector clocks to determine causality: if clock A dominates clock B (all components ≥, at least one >), A is newer. If neither dominates, the writes are concurrent and a conflict resolution policy must apply. Vector clocks give precise causality without relying on wall-clock time.”
}
},
{
“@type”: “Question”,
“name”: “When should you use last-write-wins vs three-way merge?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Last-write-wins (LWW) is simple: pick the version with the highest timestamp (or Lamport clock). Use it for commutative data (counters, presence flags) or when data loss on conflict is acceptable. Three-way merge computes a diff between each conflicting version and their common ancestor, applying non-overlapping changes automatically. Use three-way merge for structured documents (JSON, text) where both edits should be preserved. LWW is O(1); three-way merge requires storing the ancestor.”
}
},
{
“@type”: “Question”,
“name”: “How should conflict UI be presented to users?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Surface conflicts only when automatic resolution fails (true concurrent edits to the same field). Show a side-by-side diff of the conflicting versions with the common ancestor highlighted. Let the user pick one version, merge manually, or accept a server-computed merge. Store the unresolved conflict as a first-class object so the user can return to it. Never silently discard a user's edit — always preserve both versions until the user resolves.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle sync on reconnect after offline edits?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “On reconnect, the client sends its local vector clock and a delta of changes made since last sync. The server computes the diff between the client's clock and server state, returns changes the client missed, and applies the client's changes. Run conflict detection on overlapping writes. Use a sync log (append-only change feed) on the server so clients can replay missed events efficiently. Idempotency keys on each change prevent double-apply on retry.”
}
}
]
}
See also: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering