Building Offline-First Mobile Sync Engines: Architecture, Internals, and Best Practices

Mobile Development

Building Offline-First Mobile Sync Engines: Architecture, Internals, and Best Practices

A comprehensive mobile engineer's guide to local-first architecture, SQLite Outbox queues, Conflict-Free Replicated Data Types (CRDTs), optimistic UI updates, WorkManager/BackgroundTasks scheduling, and production C++/Python implementations.

In an ideal world, mobile devices maintain constant, high-speed 5G network connections. But in reality, mobile users routinely pass through subway tunnels, flight dead zones, crowded sports arenas, and spotty rural cell towers.

When a user attempts to edit a document, complete a task, or send a message without a network connection, how can mobile applications guarantee zero-latency UI responsiveness without losing data or creating unresolvable server sync conflicts? Why is traditional online-first REST API development considered an anti-pattern for modern mobile engineering? How do **Conflict-Free Replicated Data Types (CRDTs)** and **Vector Clocks** mathematically resolve concurrent edits across thousands of disconnected devices? How do Android WorkManager and iOS BackgroundTasks safely flush local outbox mutations in the background? In this deep dive, Professor Pixel breaks down offline-first mobile sync engines from first principles: local-first storage architectures, CRDT conflict resolution, step-by-step merge traces, production C++ and Python sync engines, performance benchmarks, and interactive visual simulators.


1. The Intuition: Why Online-First Mobile Apps Fail

1.1 The Network Boundary Fallacy

Traditional web and mobile applications were designed around the Online-First Paradigm: when a user performs an action (e.g. liking a post or editing a task name), the app displays a blocking loading spinner, sends a network HTTP POST request to a remote server, waits for a 200 OK response, and only then updates the user interface.

On mobile devices, this online-first approach creates severe user experience bottlenecks:

  • UI Latency Jank: Even on good 4G/5G connections, round-trip network latency ranges from 150ms to 800ms. Displaying spinners for basic text edits makes the app feel sluggish and unresponsive.
  • Data Loss in Transient Dead Zones: If the connection drops mid-request (e.g. entering an elevator), the app shows a generic "Network Request Failed" alert dialog, forcing the user to re-type their data.
  • Battery Drain from Retries: Constantly spinning cellular modems to retry failed HTTP requests rapidly drains device battery life.

1.2 The Local-First Cash Register Mental Model

To build resilient mobile applications, modern engineering teams adopt a Local-First Architecture. Think of a retail store cash register: during a power outage or internet disconnect, the store clerk does not stop selling items. The register continues recording transactions locally in a local cash drawer. When the network returns, the register synchronizes its local transaction log with the central bank server.

In a local-first mobile app:

  1. Every user mutation writes IMMEDIATELY to an embedded local database (SQLite/Room/CoreData). The UI updates instantly in $0\text{ms}$ ($60\text{ fps}$).
  2. The local database appends the mutation to a local `Outbox Queue` table.
  3. A background sync worker asynchronously flushes outbox mutations to the remote server when network connectivity is available, using delta sync protocols.
flowchart LR subgraph Mobile Device (Local First) UserUI["User Interface (0ms Response)"] ==> LocalDB["Local SQLite DB (Room/CoreData)"] LocalDB --> OutboxQueue["Local Outbox Queue Table"] OutboxQueue --> SyncWorker["Background Sync Worker (WorkManager / BGTasks)"] end subgraph Remote Cloud Infrastructure SyncWorker <== "Async Delta Sync (JSON/Protobuf)" ==> RemoteAPI["Cloud Backend & Server DB"] end style UserUI fill:#e0e7ff,stroke:#4338ca,stroke-width:2px style LocalDB fill:#dcfce7,stroke:#16a34a,stroke-width:2px style OutboxQueue fill:#fef3c7,stroke:#d97706,stroke-width:2px style SyncWorker fill:#fef3c7,stroke:#d97706,stroke-width:2px style RemoteAPI fill:#f1f5f9,stroke:#475569,stroke-width:2px

Diagram 1: Local-First Mobile Architecture. User UI reads/writes directly to local SQLite database in 0ms, while background workers handle network sync asynchronously.

Developer Pitfall — Relying on Transient Network Retries Instead of Persistent Outbox Queues:

Attempting to handle offline state by keeping HTTP network requests in-memory using OkHttp or URLSession retry interceptors is dangerous. If the user force-closes the mobile app or the operating system terminates the process due to RAM pressure, all pending in-memory requests are lost forever! Always persist pending mutations to an embedded SQLite outbox table before attempting network sync.


2. Architectural Deep Dive: Local Persistence, Delta Sync, and Vector Clocks

2.1 Embedded SQLite in Write-Ahead Logging (WAL) Mode

To ensure zero-latency UI rendering, local database operations must execute without blocking the main UI thread. SQLite configured in **Write-Ahead Logging (WAL) mode** allows concurrent readers and writers: main thread UI reads execute simultaneously while background worker threads write sync mutations to disk!

Enabling SQLite WAL Mode in Mobile Engines:
PRAGMA journal_mode=WAL; -- Enables concurrent reads during writes
PRAGMA synchronous=NORMAL; -- Optimizes disk I/O flush latency
PRAGMA foreign_keys=ON; -- Enforces relational integrity

2.2 Logical Vector Clocks for Causal Ordering

How does a distributed mobile sync engine determine which event happened first when multiple devices make edits offline? Relying on physical wall-clock timestamps (`System.currentTimeMillis()`) is flawed because mobile device clocks drift by seconds or minutes!

Instead, sync engines use **Logical Vector Clocks ($V$)**. A vector clock for $N$ devices is an array of logical counters $V = [c_1, c_2, \dots, c_N]$. Each device $i$ increments its own entry $V[i]$ whenever a local edit occurs:

$$ V_A = [2, \, 0], \quad V_B = [0, \, 1] \implies V_A \text{ and } V_B \text{ are Concurrent!} $$

When Device A receives vector clock $V_B$ from Device B during a sync merge, it updates its vector clock by taking the element-wise maximum across all counters:

$$ V_{\text{merged}}[i] = \max\left(V_A[i], \, V_B[i]\right) $$

2.3 State-based Conflict-Free Replicated Data Types (CRDTs)

A **Conflict-Free Replicated Data Type (CRDT)** is a data structure mathematically engineered to converge automatically to the exact same state across distributed devices, regardless of network message delay, reordering, or duplication.

Formally, a state-based CRDT defines a join-semilattice equipped with a partial order ($\le$) and a **LUB (Least Upper Bound) Merge Function ($\sqcup$)** that satisfies three mathematical axioms:

  • Commutativity: $X \sqcup Y = Y \sqcup X$ (Merge order does not matter).
  • Associativity: $(X \sqcup Y) \sqcup Z = X \sqcup (Y \sqcup Z)$ (Grouping does not matter).
  • Idempotency: $X \sqcup X = X$ (Merging duplicate state changes nothing).

2.4 SQLite WAL Journal File Architecture

To understand why SQLite in Write-Ahead Logging (WAL) mode enables sub-5ms local writes without blocking UI rendering threads, we must inspect the internal SQLite storage engine files:

SQLite WAL Mode Directory Files:
app_database.db (Main database file containing B-Tree pages)
app_database.db-wal (Write-Ahead Log file holding uncommitted write frames)
app_database.db-shm (Shared memory index file for concurrent reader locks)

When a user mutation occurs, SQLite appends the modified B-Tree pages directly to the end of the `-wal` file instead of overwriting the main database file. Main thread UI queries read from both the main `.db` file and the `-wal` file simultaneously using the shared memory `-shm` index. Periodically, a background thread performs a Checkpoint Operation (`PRAGMA wal_checkpoint(PASSIVE)`), flushing modified frames from `-wal` back into the main `.db` file without interrupting UI readers!


3. Under the Hood: Conflict Resolution Strategies (LWW vs CRDTs vs Server-Wins)

3.1 Evaluating Conflict Resolution Models

Mobile sync architectures employ three primary strategies to handle conflicting offline edits:

Resolution Strategy Mechanism Data Loss Risk Implementation Complexity
Last-Write-Wins (LWW) Highest timestamp/version overwrites whole record High (Overwrites concurrent field edits) Low
Field-Level LWW Register Timestamps tracked per database column Medium (Field granularity) Medium
CRDT Merge (Yjs / Automerge) Mathematical LUB ($\sqcup$) merge of operation graphs Zero (Deterministic convergence) High

3.2 Step-by-Step Numerical Walkthrough: LWW-Element-Set CRDT Merge

Let us trace how a Last-Write-Wins Element-Set (LWW-Element-Set) CRDT merges concurrent edits to a mobile task list from Device A and Device B:

Initial Task List State: {"Task-101": "Buy Milk"}
----------------------------------------------------------------
[Device A Offline] User edits title at t=10.2s:
Add Set (A): {("Task-101", "Buy Whole Milk", t=10.2)}
Remove Set (A): {}
 
[Device B Offline] User deletes task at t=10.4s:
Add Set (B): {}
Remove Set (B): {("Task-101", t=10.4)}
----------------------------------------------------------------
[Sync Engine Merge Phase (A ⊔ B)]:
1. Combine Add Sets: {("Task-101", "Buy Whole Milk", t=10.2)}
2. Combine Remove Sets: {("Task-101", t=10.4)}
3. Compare Timestamps for Task-101:
Remove timestamp (10.4s) > Add timestamp (10.2s)
4. MERGED RESULT: Task-101 is DELETED deterministically on both devices!

3.3 Operational Transformation (OT) vs CRDTs for Mobile Collaborative Editing

When building collaborative mobile applications like rich-text document editors (Google Docs, Notion), software engineers must choose between Operational Transformation (OT) and Conflict-Free Replicated Data Types (CRDTs):

  • Operational Transformation (OT): Relies on a centralized server to order operations. When Client A inserts text at index 5 and Client B inserts text at index 5 offline, the server transforms Client B's operation (`insert_at(6, 'x')`) relative to Client A's committed edit. OT requires continuous client-server communication and a single authoritative server sequencer, making peer-to-peer or offline-first operation fragile.
  • CRDTs (Yjs / Automerge): Assign unique immutable identifier tuples (`(client_id, clock_sequence)`) to every single character in the document. Because characters are positioned relative to adjacent unique IDs rather than volatile integer indices, operations can be applied in ANY order across peer-to-peer mesh networks without central server transformation!

4. Background Sync Mechanics: Android WorkManager & iOS BackgroundTasks

4.1 Android WorkManager Architecture

On Android, long-running background sync cannot run in basic background threads due to Doze Mode and background execution limits. The recommended engine is **Jetpack WorkManager**:

Android WorkManager Outbox Worker Constraints:
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
 
val syncRequest = OneTimeWorkRequestBuilder<OutboxSyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
.build()

4.3 WorkManager JobScheduler Internal State Machine

Under the hood, Jetpack WorkManager delegates background execution to Android OS system services based on API level:

  • API 23+ (Android 6.0+): Delegates to system `JobScheduler`, allowing OS Doze Mode batching across applications to conserve battery life.
  • API 14-22: Uses custom `AlarmManager` + `BroadcastReceiver` implementations to wake up background sync workers.

When a worker fails due to transient network timeouts, WorkManager applies **Jittered Exponential Backoff Retry Policy**: $Delay = \text{InitialInterval} \times 2^{\text{retry\_count}} \pm \text{Jitter}$, ensuring failing sync jobs do not hammer backend APIs during outages.

4.4 Silent Push Notifications (FCM / APNs) for Instant Remote Sync Triggering

While background polling scheduled via WorkManager flushes local outbox queues, how does a mobile client learn of updates made by OTHER users in real-time? Rather than polling the server every minute, cloud backends transmit **Silent Push Notifications** via Firebase Cloud Messaging (FCM) or Apple Push Notification service (APNs):

APNs Silent Push Notification Payload (iOS):
{
"aps": {
"content-available": 1 // Wakes up app in background without showing notification banner!
},
"sync_event": "DELTA_AVAILABLE",
"server_version": 1042
}

Developer Pitfall — Holding SQLite Locks in Expiration Handlers:

If an iOS `BGProcessingTask` expiration handler fails to immediately release open SQLite database write transactions before returning, the iOS kernel watchdog will forcibly terminate the application process with crash code `0x88880001`! Always abort background database transactions instantly inside expiration callbacks.


5. Step-by-Step Production C++ & Python Mobile Sync Engines from Scratch

5.1 Production C++ LWW-CRDT Sync Engine

1.3 Mobile Network State Monitoring (NWPathMonitor & ConnectivityManager)

To determine when to trigger background outbox flushes, mobile operating systems provide dedicated network state monitoring APIs:

  • iOS (`NWPathMonitor`): Monitors active network interfaces (`.wifi`, `.cellular`, `.wiredEthernet`) and detects expensive or constrained data connections (`isExpensive`, `isConstrained`).
  • Android (`ConnectivityManager.NetworkCallback`): Registers callbacks for `onAvailable()` and `onLost()`, tracking underlying transport capabilities like `TRANSPORT_WIFI` or `TRANSPORT_CELLULAR`.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
 
struct LWWRegister {
    std::string value;
    double timestamp;
    std::string client_id;
    
    // Deterministic CRDT Merge Function (Join-Semilattice LUB)
    void merge(const LWWRegister& incoming) {
        if (incoming.timestamp > this->timestamp) {
            this->value = incoming.value;
            this->timestamp = incoming.timestamp;
            this->client_id = incoming.client_id;
        } else if (incoming.timestamp == this->timestamp) {
            // Tie-breaker using deterministic client_id string comparison
            if (incoming.client_id > this->client_id) {
                this->value = incoming.value;
                this->client_id = incoming.client_id;
            }
        }
    }
};
 
int main() {
    LWWRegister client_A_state{"Draft V1", 100.5, "device_A"};
    LWWRegister client_B_state{"Draft V2", 102.1, "device_B"};
    
    std::cout << "[+] Before Merge (Client A): " << client_A_state.value << "\n";
    client_A_state.merge(client_B_state);
    std::cout << "[+] After Merge (Client A): " << client_A_state.value << "\n";
    return 0;
}

5.2 Production Python Outbox Sync Worker with Mock Remote Endpoint

Neither the store clerk nor the customer needs to know how the network connects—they simply perform local transactions continuously, while their local register and background sync workers handle the central bank network asynchronously!

1.3 Mobile Network State Monitoring (NWPathMonitor & ConnectivityManager)

To determine when to trigger background outbox flushes, mobile operating systems provide dedicated network state monitoring APIs:

  • iOS (`NWPathMonitor`): Monitors active network interfaces (`.wifi`, `.cellular`, `.wiredEthernet`) and detects expensive or constrained data connections (`isExpensive`, `isConstrained`).
  • Android (`ConnectivityManager.NetworkCallback`): Registers callbacks for `onAvailable()` and `onLost()`, tracking underlying transport capabilities like `TRANSPORT_WIFI` or `TRANSPORT_CELLULAR`.
import asyncio
import sqlite3
import time
 
class MobileOutboxSyncEngine:
    def __init__(self, db_path=":memory:"):
        self.conn = sqlite3.connect(db_path)
        self._init_db()
    
    def _init_db(self):
        with self.conn:
            self.conn.execute("""
                CREATE TABLE IF NOT EXISTS outbox_mutations (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    record_id TEXT,
                    payload TEXT,
                    timestamp REAL,
                    synced INTEGER DEFAULT 0
                )
            """)
    
    def enqueue_mutation(self, record_id, payload):
        with self.conn:
            self.conn.execute(
                "INSERT INTO outbox_mutations (record_id, payload, timestamp) VALUES (?, ?, ?)",
                (record_id, payload, time.time())
            )
        print(f"[Local DB] Enqueued local mutation for record: {record_id}")
    
    async def flush_outbox(self):
        cursor = self.conn.cursor()
        cursor.execute("SELECT id, record_id, payload FROM outbox_mutations WHERE synced = 0")
        pending = cursor.fetchall()
        if not pending:
            print("[Sync Worker] Outbox empty. Zero pending mutations.")
            return
        
        print(f"[Sync Worker] Flushing {len(pending)} pending mutations to cloud API...")
        await asyncio.sleep(0.5) # Simulate HTTP POST Batch Network Call
        
        synced_ids = [row[0] for row in pending]
        with self.conn:
            self.conn.executemany("UPDATE outbox_mutations SET synced = 1 WHERE id = ?", [(i,) for i in synced_ids])
        print(f"[Sync Worker] Successfully synced mutations: {synced_ids}")
 
# Usage Demonstration
async def main():
    engine = MobileOutboxSyncEngine()
    engine.enqueue_mutation("note_101", '{"title": "Offline Edit 1"}')
    engine.enqueue_mutation("note_102", '{"title": "Offline Edit 2"}')
    await engine.flush_outbox()
 
asyncio.run(main())

Developer Pitfall — Executing Outbox Operations Outside Explicit SQLite Transactions:

If your mobile sync worker updates your local entity table (`notes`) and inserts into `outbox_mutations` in two separate database calls without wrapping them in `BEGIN TRANSACTION ... COMMIT`, an application crash between the two statements will corrupt local cache integrity! Always perform local entity updates and outbox insertions atomically.


6. Advanced: Optimistic UI Updates, Soft Deletions, and Database Schema Migrations

6.1 Optimistic UI Updates and Rollback Strategies

To deliver immediate feedback, local-first apps apply Optimistic UI Updates: when a user clicks "Delete Task", the item disappears from the screen instantly. However, if the server subsequently rejects the mutation (e.g. due to permission authorization failure), the sync engine must perform an graceful Optimistic UI Rollback:

Optimistic UI State Lifecycle:
1. User performs action -> Local DB updated -> UI re-renders instantly (State: OPTIMISTIC_LOCAL)
2. Outbox Worker sends payload to server API -> Server returns 403 FORBIDDEN
3. Sync Engine catches rejection -> Restores pre-mutation entity state -> Emits UI Toast Alert

6.2 Why Hard Deletes Break Offline Sync (Tombstones Pattern)

Executing a physical SQL `DELETE FROM tasks WHERE id = 'task_101'` on an offline device destroys record history. When the device reconnects and fetches delta changes from the server, the server sees that the client is missing `task_101` and sends it back to the device—resurrecting the deleted task!

Offline sync engines require Tombstones (Soft Deletions): instead of physically deleting rows, the database sets a `deleted_at` timestamp flag (`UPDATE tasks SET deleted_at = CURRENT_TIMESTAMP WHERE id = 'task_101'`). Tombstone records propagate across devices during delta sync, ensuring all clients learn of the deletion!

6.3 Managing Database Schema Migrations on Disconnected Mobile Devices

One of the most challenging aspects of local-first mobile development is supporting database DDL schema migrations when clients remain offline for extended periods. If an app updates from database version 5 to version 8 while a device is offline, the local sync engine must handle schema evolution seamlessly:

  • Sequential Migration Execution: Never skip database versions. The mobile ORM (Room/CoreData/WatermelonDB) must execute incremental migration blocks (`v5 -> v6`, `v6 -> v7`, `v7 -> v8`) sequentially, preserving un-synced outbox mutations in the local SQLite file.
  • Backward-Compatible API Payload Versioning: Server sync API endpoints must accept outbox mutations formatted under older client payload schemas, using server-side transformation middleware to convert legacy payloads into the current server database schema.

Developer Pitfall — Premature Garbage Collection of Tombstone Records:

If your cloud backend executes a database cleanup job that purges `deleted_at` tombstone records after 7 days, a mobile client that has been offline for 10 days will miss the deletion tombstone during its next delta sync, re-uploading the deleted item to the cloud! Tombstones must be retained for at least as long as your maximum allowed client offline sync window (e.g. 30 days).


7. Industry Comparison Matrix of Mobile Offline Sync Solutions

The table below compares core mobile offline sync engines and data layer frameworks across architectural parameters:

Sync Engine / Framework Data Architecture Conflict Resolution Local UI Latency Primary Mobile Use Case
Custom SQLite Outbox Engine Embedded SQLite + Outbox Queue Custom LWW / Vector Clocks < 5ms (Local Direct) Custom Enterprise Mobile Apps (Slack, Notion)
WatermelonDB (React Native) Lazy SQLite / IndexedDB Proxy Delta-based Timestamp Merge < 10ms High-Performance Cross-Platform React Native Apps
ElectricSQL / PowerSync Postgres-to-SQLite Sync Engine Rich CRDTs (Rich-Text / LWW) < 2ms Modern Local-First Collaborative Software (Linear-style)
Realm / Atlas Device Sync NoSQL Object Database Engine Operational Transformation Merge < 8ms MongoDB-Backed Mobile Workloads

8. Interactive: Offline Sync & CRDT Conflict Resolution Simulator

Click "Step Offline Sync" to simulate Device A Offline Edit $\to$ Enqueue Outbox $\to$ Reconnect Network $\to$ CRDT Merge:

Simulator Idle. Click button to step through offline sync...
1. OFFLINE MUTATION (Write to Local SQLite DB + Outbox Table)
Idle
2. NETWORK RECONNECT (WorkManager Flushes Outbox Batch)
Idle
3. CRDT MERGE CONVERGENCE (Deterministic LUB State Sync)
Idle

9. Performance Benchmarks: Local-First UI Response vs Online REST API Latency

The chart below compares UI execution latency (milliseconds) for user interactions between Online-First REST architecture and Local-First SQLite Outbox architecture:


10. Frequently Asked Questions

Q1: What is the core architectural principle of Local-First Mobile Development?

Local-First Development mandates that every user mutation reads and writes directly to an embedded local database (like SQLite/Room/CoreData) in 0ms, updating the UI instantly. Network synchronization occurs asynchronously in the background via persistent Outbox queues when network connections are available.

Q2: Why is using physical system clocks (`System.currentTimeMillis()`) dangerous for conflict resolution?

Mobile device system clocks suffer from physical clock drift, manual user clock changes, and time-zone adjustments. If a device clock is set 10 minutes ahead, its mutation timestamps will permanently overwrite edits made by other clients. Sync engines use logical Vector Clocks to track true causal order.

Q3: What is a Conflict-Free Replicated Data Type (CRDT)?

A CRDT is a mathematical data structure that can be edited independently on multiple disconnected devices and merged deterministically without central locking. CRDT merge functions are mathematically proven to be commutative, associative, and idempotent.

Q4: Why do offline sync engines use Tombstones instead of hard SQL `DELETE` queries?

Executing a physical `DELETE` query erases the record locally without leaving a trace. When the client reconnects and fetches delta changes from the server, the server sees that the client is missing the item and re-downloads it, resurrecting the deleted record. Soft deletion tombstones (`deleted_at`) propagate the deletion intent to all clients.

Q5: How does Android WorkManager ensure outbox mutations are flushed reliably?

Jetpack WorkManager persists background sync tasks in an internal SQLite database managed by the OS. It monitors system constraints (e.g., `NetworkType.CONNECTED`) and executes background sync workers even if the user force-closes the app or restarts the device.

Q6: What is an Optimistic UI Update and how is a rejection handled?

An Optimistic UI Update updates the user interface instantly before the server acknowledges the change. If the server subsequently rejects the mutation (e.g. due to authorization rules), the sync engine rolls back the local SQLite record to its previous state and notifies the user with a Toast alert.

Q7: Why is SQLite Write-Ahead Logging (WAL) mode mandatory for local-first apps?

In default journal mode, SQLite locks the database file during writes, blocking main-thread UI database reads and causing UI frame drops. WAL mode separates reads from writes, allowing background outbox workers to write sync data while the main thread renders the UI smoothly.

Q8: How do mobile apps manage database DDL schema migrations for clients offline for weeks?

Local-first apps use incremental versioned SQLite migration scripts (`Migration(1, 2)`, `Migration(2, 3)`). When the app updates, Room or CoreData executes pending migration steps sequentially to bring the local schema up to date before running the sync engine.

Q9: What is the difference between State-based CRDTs and Operation-based CRDTs?

State-based CRDTs (CvRDTs) send the entire local data structure state during sync, merging states using a join-semilattice LUB function. Operation-based CRDTs (CmRDTs) send individual granular mutation operations (`insert_at(index=4, char='a')`), requiring reliable causal message delivery channels.

Q10: What real-world mobile applications rely on offline-first sync engines?

Offline-first sync architectures power flagship applications like Slack (offline message drafting), Notion (local document editing), WhatsApp (encrypted offline messaging outbox), and Linear (local issue tracking).

Q11: How does a Delta Sync protocol minimize mobile network bandwidth consumption?

A Delta Sync protocol transmits only modified database field deltas or operational mutations instead of re-downloading entire database records. Each mobile client tracks a local `last_synced_version` checkpoint, requesting only server changes with `version > last_synced_version` during sync polls.

Q12: What is the role of Vector Clocks in detecting concurrent offline mobile edits?

Vector Clocks maintain a list of logical version counters across distributed devices ($[v_A, v_B]$). By comparing vector clocks, the sync engine can mathematically determine whether Edit A happened before Edit B ($V_A < V_B$), after Edit B ($V_A > V_B$), or concurrently ($V_A \parallel V_B$), triggering CRDT merge rules for concurrent edits.

Q13: How does SQLite WAL mode prevent main-thread UI lag during background sync writes?

In SQLite WAL mode, write operations append modified frames to a separate `-wal` file without locking the main `.db` database file. This allows background threads to write sync mutations to disk while the main UI thread executes concurrent read queries without waiting for disk locks.

Post a Comment

Previous Post Next Post