{"id":21,"date":"2026-01-28T20:37:23","date_gmt":"2026-01-28T20:37:23","guid":{"rendered":"https:\/\/blog.apexo.app\/?p=21"},"modified":"2026-01-28T20:39:58","modified_gmt":"2026-01-28T20:39:58","slug":"ts-lww-sync-a-pragmatic-offline-first-synchronization-algorithm","status":"publish","type":"post","link":"https:\/\/blog.apexo.app\/?p=21","title":{"rendered":"TS\u2011LWW Sync: A Pragmatic Offline\u2011First Synchronization Algorithm"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\"><\/h1>\n\n\n\n<p>Modern applications live in a messy world.<\/p>\n\n\n\n<p>Users go offline. Networks lie. Devices crash. Two people edit the same record at the same time. Files are uploaded halfway and then abandoned. Yet expectations are brutal: <em>the app must feel instant, never lose data, and always recover<\/em>.<\/p>\n\n\n\n<p>This post describes <strong>TS\u2011LWW Sync<\/strong> (Timestamped Last\u2011Writer\u2011Wins Sync), a synchronization algorithm designed for real-world CRUD applications \u2014 especially offline\u2011first systems like a <strong>dental clinic CRM (Apexo)<\/strong> \u2014 where correctness, speed, and simplicity matter more than academic perfection.<\/p>\n\n\n\n<p>The goal of TS\u2011LWW Sync is not to be clever.<br>The goal is to be <strong>reliable, fast, cheap, and understandable<\/strong>.<\/p>\n\n\n\n<!--more-->\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Core Idea<\/h2>\n\n\n\n<p>TS\u2011LWW Sync is built on one simple observation:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>If every change has a timestamp, then synchronization can be reduced to <em>&#8220;what changed after time X&#8221;<\/em>.<\/p>\n<\/blockquote>\n\n\n\n<p>Instead of tracking per\u2011record versions, diffs, or complex merge trees, the system tracks:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A <strong>single version number per store<\/strong> (the latest known timestamp)<\/li>\n\n\n\n<li>A <strong>local deferred queue<\/strong> for offline changes<\/li>\n\n\n\n<li>A <strong>deterministic conflict rule<\/strong> (Last Writer Wins)<\/li>\n<\/ul>\n\n\n\n<p>Everything else falls out naturally.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Naming the Algorithm<\/h2>\n\n\n\n<p><strong>TS\u2011LWW Sync<\/strong> stands for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>TS<\/strong> \u2014 Timestamp\u2011based<\/li>\n\n\n\n<li><strong>LWW<\/strong> \u2014 Last Writer Wins<\/li>\n<\/ul>\n\n\n\n<p>It is best described as a:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><em>Store\u2011versioned, timestamp\u2011driven, offline\u2011first synchronization algorithm with deferred writes.<\/em><\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">How It Works (Conceptually)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Store\u2011Level Versioning<\/h3>\n\n\n\n<p>Instead of versioning every record individually, the system keeps <strong>one version number for the entire dataset<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The version is the <strong>maximum timestamp<\/strong> of all known records<\/li>\n\n\n\n<li>This timestamp is updated only after a successful sync<\/li>\n<\/ul>\n\n\n\n<p>This allows the client to ask a very cheap question:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cGive me everything that changed after version X.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>No scans. No diffs. No reconciliation passes.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">2. Local\u2011First Writes<\/h3>\n\n\n\n<p>All user actions are applied <strong>locally first<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>UI updates instantly<\/li>\n\n\n\n<li>Data is persisted locally<\/li>\n\n\n\n<li>Changes are marked as <em>pending<\/em><\/li>\n<\/ul>\n\n\n\n<p>If the network is available, the system <em>attempts<\/em> to push immediately.<br>If not, the change is safely deferred.<\/p>\n\n\n\n<p>From the user\u2019s perspective:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>The app never blocks. Ever.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">3. Deferred Queue (Offline Engine)<\/h3>\n\n\n\n<p>When offline, changes are placed into a <strong>deferred queue<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Each deferred item is stored with its local timestamp<\/li>\n\n\n\n<li>Deferred operations survive restarts<\/li>\n\n\n\n<li>Multiple edits collapse into the latest value<\/li>\n<\/ul>\n\n\n\n<p>This queue acts as a lightweight <strong>write\u2011ahead log<\/strong>.<\/p>\n\n\n\n<p>When connectivity returns, deferred changes are replayed deterministically.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">4. Pull\u2011Then\u2011Resolve Sync<\/h3>\n\n\n\n<p>During synchronization:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The client fetches all remote updates since its last version<\/li>\n\n\n\n<li>Deferred local changes are compared against remote updates<\/li>\n\n\n\n<li>Conflicts are detected <em>only where necessary<\/em><\/li>\n<\/ol>\n\n\n\n<p>Conflict rule:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Whichever side has the newer timestamp wins<\/strong><\/p>\n<\/blockquote>\n\n\n\n<p>No heuristics. No guessing. No silent corruption.<\/p>\n\n\n\n<p>Conflicts are counted, observable, and predictable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">5. Files Are First\u2011Class Citizens<\/h3>\n\n\n\n<p>Binary files (images, documents) are <strong>not treated as records<\/strong>.<\/p>\n\n\n\n<p>Instead, they are handled as:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Explicit operations (upload \/ delete)<\/li>\n\n\n\n<li>Independently retryable<\/li>\n\n\n\n<li>Idempotent<\/li>\n<\/ul>\n\n\n\n<p>This avoids the most common sync bug in business apps:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><em>&#8220;The data synced, but the images didn\u2019t.&#8221;<\/em><\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">6. Realtime Is a Signal, Not a Transport<\/h3>\n\n\n\n<p>Realtime updates do <strong>not<\/strong> push data directly.<\/p>\n\n\n\n<p>They simply say:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cSomething changed \u2014 you might want to sync.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>This keeps the system robust even when realtime messages are dropped, duplicated, or delayed.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Is Fast<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">CPU Usage<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No diff computation<\/li>\n\n\n\n<li>No tree merging<\/li>\n\n\n\n<li>No per\u2011record version checks<\/li>\n\n\n\n<li>No background reconciliation loops<\/li>\n<\/ul>\n\n\n\n<p>Sync work is <strong>O(changes)<\/strong>, not <strong>O(total records)<\/strong>.<\/p>\n\n\n\n<p>In idle state, CPU usage is effectively zero.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Network Usage<\/h3>\n\n\n\n<p>TS\u2011LWW Sync is extremely network\u2011efficient:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pulls only records updated since last sync<\/li>\n\n\n\n<li>Pushes only modified records<\/li>\n\n\n\n<li>Batches writes<\/li>\n\n\n\n<li>Avoids redundant retries<\/li>\n<\/ul>\n\n\n\n<p>Typical sync payloads are <strong>tiny<\/strong>, even with large datasets.<\/p>\n\n\n\n<p>This makes it ideal for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Mobile networks<\/li>\n\n\n\n<li>Unreliable Wi\u2011Fi<\/li>\n\n\n\n<li>Clinics with weak infrastructure<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Memory Footprint<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No full history retained<\/li>\n\n\n\n<li>No shadow copies<\/li>\n\n\n\n<li>No conflict trees<\/li>\n<\/ul>\n\n\n\n<p>Memory usage scales linearly with current data size, not edit history.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Real\u2011World Scenarios<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 1: Dentist Goes Offline<\/h3>\n\n\n\n<p>A dentist edits multiple patient records while offline.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>UI responds instantly<\/li>\n\n\n\n<li>Changes are saved locally<\/li>\n\n\n\n<li>No sync attempts block the workflow<\/li>\n<\/ul>\n\n\n\n<p>When connectivity returns, everything syncs automatically.<\/p>\n\n\n\n<p>No lost work. No manual recovery.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 2: Two Assistants Edit the Same Patient<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Assistant A edits the record at 10:01<\/li>\n\n\n\n<li>Assistant B edits the same record at 10:03<\/li>\n<\/ul>\n\n\n\n<p>Result:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Assistant B\u2019s change wins<\/li>\n\n\n\n<li>Assistant A\u2019s client pulls the update<\/li>\n\n\n\n<li>Conflict is counted, not hidden<\/li>\n<\/ul>\n\n\n\n<p>Predictable. Transparent. Debbugable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Scenario 3: Image Upload Fails Midway<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Data sync succeeds<\/li>\n\n\n\n<li>Image upload fails<\/li>\n<\/ul>\n\n\n\n<p>The image operation is retried independently.<br>No corrupted state.<br>No dangling references.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Fits a Dental Clinic CRM (Apexo)<\/h2>\n\n\n\n<p>Dental clinic systems have unique requirements:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Intermittent connectivity<\/li>\n\n\n\n<li>Multiple devices per clinic<\/li>\n\n\n\n<li>Small teams editing shared data<\/li>\n\n\n\n<li>Heavy use of images (X\u2011rays, photos)<\/li>\n\n\n\n<li>Zero tolerance for data loss<\/li>\n<\/ul>\n\n\n\n<p>TS\u2011LWW Sync is a natural fit because:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Conflicts are rare and acceptable<\/li>\n\n\n\n<li>Data volume is moderate<\/li>\n\n\n\n<li>Latency matters more than perfect merges<\/li>\n\n\n\n<li>Staff should <em>never<\/em> think about syncing<\/li>\n<\/ul>\n\n\n\n<p>In practice, the system feels:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><em>Instant when offline, invisible when online.<\/em><\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What This Algorithm Is (and Isn\u2019t)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">It Is:<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Deterministic<\/li>\n\n\n\n<li>Fast<\/li>\n\n\n\n<li>Cheap to operate<\/li>\n\n\n\n<li>Easy to reason about<\/li>\n\n\n\n<li>Easy to debug<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">It Is Not:<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A CRDT system<\/li>\n\n\n\n<li>A collaborative editor<\/li>\n\n\n\n<li>A full event\u2011sourcing engine<\/li>\n<\/ul>\n\n\n\n<p>And that\u2019s a <strong>feature<\/strong>, not a limitation.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Final Thoughts<\/h2>\n\n\n\n<p>TS\u2011LWW Sync embraces a simple truth:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Most business apps do not need perfect merges.<br>They need <strong>reliability, speed, and clarity<\/strong>.<\/p>\n<\/blockquote>\n\n\n\n<p>By choosing store\u2011level versioning, timestamp\u2011based pulls, and deferred writes, this algorithm delivers exactly that \u2014 without complexity tax.<\/p>\n\n\n\n<p>For systems like <strong>Apexo<\/strong>, where real people rely on real data every day, this approach keeps the focus where it belongs:<\/p>\n\n\n\n<p><strong>on the work \u2014 not on the sync.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><em>If you\u2019re building an offline\u2011first business application and want a sync system that behaves like a calm adult instead of a fragile genius, TS\u2011LWW Sync is worth considering.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How the synchronization algorithm works (conceptual overview)<\/h2>\n\n\n\n<p>This synchronization algorithm is designed for <strong>offline-first applications<\/strong> where multiple clients may modify the same data independently and then reconnect later. The core goals are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No central lock or coordination<\/li>\n\n\n\n<li>Deterministic conflict resolution<\/li>\n\n\n\n<li>Minimal metadata<\/li>\n\n\n\n<li>Idempotent and repeatable sync<\/li>\n<\/ul>\n\n\n\n<p>At a high level, each record carries <strong>versioning metadata<\/strong> that allows the system to decide which change should win when conflicts occur.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Logical versioning (timestamp-based)<\/h3>\n\n\n\n<p>Each record has an <code>updated<\/code> value that represents the <strong>logical time of the last modification<\/strong>. This is not meant to be a perfect wall-clock time; it is simply a monotonic value that increases whenever the record is modified.<\/p>\n\n\n\n<p>This timestamp is the primary comparison tool during synchronization.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Store identity (tie-breaker)<\/h3>\n\n\n\n<p>Every client or store has a unique <code>storeId<\/code>. When two records have the <strong>same logical timestamp<\/strong>, the <code>storeId<\/code> is used as a deterministic tie-breaker.<\/p>\n\n\n\n<p>This guarantees that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Conflicts are resolved the same way on all devices<\/li>\n\n\n\n<li>No additional coordination is needed<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">3. Last-write-wins with deterministic ordering<\/h3>\n\n\n\n<p>When syncing two versions of the same record:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Compare <code>updated<\/code><\/li>\n\n\n\n<li>The record with the higher value wins<\/li>\n\n\n\n<li>If equal, compare <code>storeId<\/code><\/li>\n\n\n\n<li>The record with the higher <code>storeId<\/code> wins<\/li>\n<\/ol>\n\n\n\n<p>This creates a <strong>total ordering<\/strong> across all updates, which is crucial for convergence.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Soft deletes (tombstones)<\/h3>\n\n\n\n<p>Instead of physically deleting records, the algorithm uses a <code>deleted<\/code> flag.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Deletions are treated as normal updates<\/li>\n\n\n\n<li>A delete can override an older update<\/li>\n\n\n\n<li>Tombstones prevent deleted data from reappearing during sync<\/li>\n<\/ul>\n\n\n\n<p>This makes deletion <strong>sync-safe and reversible<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Incremental synchronization<\/h3>\n\n\n\n<p>Rather than syncing the entire dataset every time, each client tracks the <strong>last known sync point per store<\/strong>.<\/p>\n\n\n\n<p>During sync:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Only records updated after the last sync point are exchanged<\/li>\n\n\n\n<li>This keeps bandwidth usage low<\/li>\n\n\n\n<li>The process is idempotent: syncing twice produces the same result<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">6. Convergence guarantee<\/h3>\n\n\n\n<p>Because conflict resolution is:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Deterministic<\/li>\n\n\n\n<li>Based on immutable metadata<\/li>\n\n\n\n<li>Applied symmetrically on all peers<\/li>\n<\/ul>\n\n\n\n<p>All replicas will eventually converge to the same state, regardless of sync order or frequency.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>Modern applications live in a messy world. Users go offline. Networks lie. Devices crash. Two people edit the same record at the same time. Files are uploaded halfway and then abandoned. Yet expectations are brutal: the app must feel instant, never lose data, and always recover. This post describes TS\u2011LWW Sync (Timestamped Last\u2011Writer\u2011Wins Sync), a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-21","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/blog.apexo.app\/index.php?rest_route=\/wp\/v2\/posts\/21","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.apexo.app\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.apexo.app\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.apexo.app\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.apexo.app\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=21"}],"version-history":[{"count":3,"href":"https:\/\/blog.apexo.app\/index.php?rest_route=\/wp\/v2\/posts\/21\/revisions"}],"predecessor-version":[{"id":24,"href":"https:\/\/blog.apexo.app\/index.php?rest_route=\/wp\/v2\/posts\/21\/revisions\/24"}],"wp:attachment":[{"href":"https:\/\/blog.apexo.app\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=21"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.apexo.app\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=21"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.apexo.app\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=21"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}