External Publication
Visit Post

Conflict Resolution in a Bidirectional Sync App — How I Handle the Hard Cases

DEV Community [Unofficial] June 19, 2026
Source

All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

HiyokoAutoSync does bidirectional sync between Android and Mac. Bidirectional sync has a hard problem: what happens when the same file is modified on both sides? Here's how I handle it.

The conflict cases

  1. File modified on both sides since last sync — which version wins?
  2. File deleted on one side, modified on the other — delete or keep?
  3. File moved on one side — move on the other side or treat as delete + create?

Most sync apps punt on cases 1 and 3. Here's my approach.

Detecting conflicts

Track last-synced state in SQLite:

CREATE TABLE sync_state (
    file_path TEXT PRIMARY KEY,
    mac_hash TEXT,
    android_hash TEXT,
    mac_modified INTEGER,
    android_modified INTEGER,
    last_synced INTEGER
);

On sync check:

fn classify_file(record: &SyncRecord, mac_stat: &FileStat, android_stat: &FileStat) -> SyncAction {
    let mac_changed = mac_stat.hash != record.mac_hash;
    let android_changed = android_stat.hash != record.android_hash;

    match (mac_changed, android_changed) {
        (true, false)  => SyncAction::CopyToAndroid,
        (false, true)  => SyncAction::CopyToMac,
        (false, false) => SyncAction::NoOp,
        (true, true)   => SyncAction::Conflict,
    }
}

Conflict resolution strategies

I offer three strategies, user-configurable:

Newer wins : compare modification timestamps, keep the more recent file.

SyncAction::Conflict => {
    if mac_stat.modified > android_stat.modified {
        SyncAction::CopyToAndroid
    } else {
        SyncAction::CopyToMac
    }
}

Mac always wins : for users who treat Mac as source of truth.

Keep both : rename one file with a conflict suffix, keep both versions.

// Rename Android version to "file.conflict-2026-05-01.ext"
let conflict_name = add_conflict_suffix(&file_path);
copy_to_mac_as(&android_file, &conflict_name)?;
copy_to_android(&mac_file)?;

Delete vs modify conflict

File deleted on Mac, modified on Android:

(Deleted, Modified) => {
    // Default: keep the modified file, restore it on Mac
    // Alternative: delete from both sides
    // User configurable
    SyncAction::RestoreToMac
}

The safe default is to keep data. Deleting across both sides on a conflict can cause data loss users didn't intend.

The verdict

Bidirectional sync without conflict resolution is a bug waiting to happen. The "newer wins" strategy covers 90% of real-world cases. Keep both covers the rest. Make it configurable for power users.

TL;DR: Track sync state (hash + modified time) in SQLite per file. Classify each file into CopyToAndroid, CopyToMac, NoOp, or Conflict using a match on what changed. Offer three strategies: newer wins, Mac wins, or keep both. For delete vs modify conflicts, default to keeping data — accidental deletion is worse than a duplicate.

If this was useful, a ❤️ helps more than you'd think — thanks!

HiyokoAutoSync | X → @hiyoyok

Discussion in the ATmosphere

Loading comments...