Loading learning content...
Conflict resolution is the most challenging aspect of distributed file synchronization. When the same file is modified on multiple devices simultaneously, the system faces a fundamental question: Which version is correct?
Unlike traditional client-server systems where the server always has the latest version, cloud storage systems allow concurrent modifications across many devices. A user might edit a document on their laptop, while a colleague edits the same document on their desktop, while their phone is offline with its own cached version. When these devices sync, conflicts emerge.
The Goal of Conflict Resolution:
Never lose user data. When conflicts occur, preserve all work and help users reconcile differences with minimal confusion.
By the end of this page, you'll understand: (1) How to reliably detect conflicts in distributed systems, (2) Different resolution strategies and their trade-offs, (3) Automatic vs manual resolution and when to use each, (4) Operational Transformation (OT) and CRDTs for real-time collaboration, and (5) How production systems handle edge cases.
Before resolving conflicts, we must detect them. Conflict detection requires tracking the history of modifications and identifying when changes diverge from a common ancestor.
Version Vectors:
The most robust approach uses version vectors (vector clocks). Each device maintains a version number, and the combined vector represents the full history:
Version Vector Example:
Device A creates file: {A:1}
Device A modifies: {A:2}
Device A syncs to B: B receives {A:2}
Device B modifies: {A:2, B:1}
Now if Device A modifies offline: {A:3}
And Device B modifies offline: {A:2, B:2}
These versions CONFLICT because:
- A:3 has A:3 > A:2 but doesn't know about B:2
- A:2,B:2 has B:2 > B:1 but doesn't know about A:3
- Neither "happens before" the other
| Method | Complexity | Information Captured | Use Case |
|---|---|---|---|
| Server Revision | Low | Linear sequence only | Simple file storage (Dropbox model) |
| Version Vectors | Medium | Full causality graph | Collaborative editing, complex merge |
| Content Hash | Low | Current state only | Verification, deduplication |
| Merkle Tree | High | Structural differences | Large directory sync, git-like systems |
1234567891011121314151617181920212223242526272829303132333435363738
// Server revision-based conflict detectioninterface FileVersion { path: string; revision: number; // Server-assigned, monotonically increasing contentHash: string; // SHA-256 of file content parentRevision: number; // The revision this was based on} class ConflictDetector { // Called when client attempts to upload detectConflict( serverVersion: FileVersion, clientUpload: { parentRevision: number; contentHash: string } ): 'no_conflict' | 'conflict' | 'no_change' { // Client is up-to-date in sync with server if (clientUpload.parentRevision === serverVersion.revision) { if (clientUpload.contentHash === serverVersion.contentHash) { return 'no_change'; // Same content, no upload needed } return 'no_conflict'; // Normal upload proceeds } // Client's parent doesn't match server's current revision // Server has been modified since client's last sync if (clientUpload.parentRevision < serverVersion.revision) { // But content is same? No real conflict if (clientUpload.contentHash === serverVersion.contentHash) { return 'no_change'; } return 'conflict'; // True conflict: different content } // Client claims newer parent than server knows? // This shouldn't happen - indicates client bug throw new Error('Invalid state: client revision ahead of server'); }}Notice the 'no_change' case: if two users make the same changes independently, there's technically a conflict (divergent edits) but effectively no conflict (same result). Smart systems detect this and skip unnecessary conflict resolution, improving user experience.
Once a conflict is detected, the system must resolve it. Different strategies have different trade-offs between simplicity, user experience, and data preservation.
Conflict Copy Naming Conventions:
Dropbox: "report.docx (John's conflicted copy 2024-01-15).docx"
Google: "report.docx" stays, older becomes "report.docx.backup"
OneDrive: "report-LAPTOP-JOHN.docx"
iCloud: "report 2.docx"
Best Practice: Include user/device and timestamp
Pattern: "filename (Device's conflicted copy YYYY-MM-DD HH:mm).ext"
| File Type | Recommended Strategy | Rationale |
|---|---|---|
| Binary files (images, videos) | Copy-Both | Cannot be automatically merged |
| Text files (code, notes) | 3-way merge, fallback to copy-both | Often mergeable, fall back if conflicts |
| Documents (docx, pdf) | Copy-Both with visual diff | Format-specific merge too complex |
| Realtime docs (Google Docs) | OT or CRDT | Designed for concurrent editing |
| Database files | Application-specific | Requires semantic understanding |
| Config files | 3-way merge + validation | Merge then validate syntax |
Three-way merge is the most sophisticated automatic resolution technique for text files. It uses the common ancestor to determine what each version changed, then combines non-conflicting changes.
How Three-Way Merge Works:
Common Ancestor (Base): Version A (Local): Version B (Remote):
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Line 1: Hello │ │ Line 1: Hello │ │ Line 1: Hello │
│ Line 2: World │ │ Line 2: World │ │ Line 2: Everyone │ ← B changed
│ Line 3: Foo │ │ Line 3: Bar │ ← │ Line 3: Foo │ A changed
│ Line 4: End │ │ Line 4: End │ │ Line 4: End │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Merge Analysis:
- Line 1: Same in all → keep
- Line 2: A=Base, B changed → use B's version
- Line 3: A changed, B=Base → use A's version
- Line 4: Same in all → keep
Merged Result:
┌─────────────────────┐
│ Line 1: Hello │
│ Line 2: Everyone │ ← From B
│ Line 3: Bar │ ← From A
│ Line 4: End │
└─────────────────────┘
✓ Merge successful! Both changes preserved.
When Three-Way Merge Fails (True Conflict):
Base: Version A: Version B:
│ Line 2: World │ Line 2: Earth │ Line 2: Everyone
↓ ↓
A changed to Earth B changed to Everyone
BOTH changed the same line differently!
Result: CONFLICT - cannot auto-merge
Typical output:
┌─────────────────────┐
│ Line 1: Hello │
│ <<<<<<< LOCAL │
│ Line 2: Earth │
│ ======= │
│ Line 2: Everyone │
│ >>>>>>> REMOTE │
│ Line 3: Bar │
└─────────────────────┘
User must manually resolve the conflict markers.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
// Simplified three-way merge algorithmtype MergeResult = | { status: 'success'; content: string[] } | { status: 'conflict'; conflicts: ConflictRegion[] }; interface ConflictRegion { lineStart: number; localContent: string[]; remoteContent: string[];} function threeWayMerge( base: string[], local: string[], remote: string[]): MergeResult { // Compute differences const localDiff = computeDiff(base, local); // What local changed const remoteDiff = computeDiff(base, remote); // What remote changed const result: string[] = []; const conflicts: ConflictRegion[] = []; let baseIdx = 0, localIdx = 0, remoteIdx = 0; while (baseIdx < base.length || localIdx < local.length || remoteIdx < remote.length) { const localChanged = localDiff.hasChange(baseIdx); const remoteChanged = remoteDiff.hasChange(baseIdx); if (!localChanged && !remoteChanged) { // Neither changed - keep base result.push(base[baseIdx]); baseIdx++; localIdx++; remoteIdx++; } else if (localChanged && !remoteChanged) { // Only local changed - use local const change = localDiff.getChange(baseIdx); result.push(...change.newLines); baseIdx += change.baseLines; localIdx += change.newLines.length; remoteIdx += change.baseLines; } else if (!localChanged && remoteChanged) { // Only remote changed - use remote const change = remoteDiff.getChange(baseIdx); result.push(...change.newLines); baseIdx += change.baseLines; localIdx += change.baseLines; remoteIdx += change.newLines.length; } else { // BOTH changed - check if same change const localChange = localDiff.getChange(baseIdx); const remoteChange = remoteDiff.getChange(baseIdx); if (arraysEqual(localChange.newLines, remoteChange.newLines)) { // Same change - no conflict result.push(...localChange.newLines); } else { // Different changes - TRUE CONFLICT conflicts.push({ lineStart: result.length, localContent: localChange.newLines, remoteContent: remoteChange.newLines, }); // Add conflict markers result.push('<<<<<<< LOCAL'); result.push(...localChange.newLines); result.push('======='); result.push(...remoteChange.newLines); result.push('>>>>>>> REMOTE'); } baseIdx += Math.max(localChange.baseLines, remoteChange.baseLines); localIdx += localChange.newLines.length; remoteIdx += remoteChange.newLines.length; } } return conflicts.length > 0 ? { status: 'conflict', conflicts } : { status: 'success', content: result };}Simple three-way merge doesn't understand semantics. If Alice adds a function foo() and Bob also adds a different foo() in different locations, merge succeeds but code won't compile! Advanced systems can detect such semantic conflicts through AST analysis or test execution.
For real-time collaborative editing (Google Docs, Figma, Notion), file-level conflict resolution isn't sufficient. We need character-level or element-level conflict handling that works in real-time. Two approaches dominate: Operational Transformation (OT) and Conflict-free Replicated Data Types (CRDTs).
Operational Transformation (OT):
OT works by transforming operations against concurrent operations to preserve intent:
Initial: "HELLO"
User A: INSERT 'X' at position 1 → "HXELLO"
User B: INSERT 'Y' at position 3 → "HELYLO"
Problem: If we apply A's operation to B's result:
"HELYLO" + INSERT 'X' at position 1 → "HXELYLO"
But B expected position 3 to be after 'L'!
OT Solution: Transform B's operation against A's:
A inserted at 1, which is before B's position 3
So B's position shifts: 3 + 1 = 4
New B operation: INSERT 'Y' at position 4
Apply in sequence:
"HELLO" → "HXELLO" → "HXELYLO"
Both insertions preserved at intended locations! ✓
CRDT Example - G-Counter (Grow-only Counter):
// Each node maintains its own counter
Node A: {A: 5, B: 3} // A incremented 5 times, knows B did 3
Node B: {A: 2, B: 7} // B incremented 7 times, knows A did 2
Merge: Take maximum of each component
{A: max(5,2), B: max(3,7)} = {A: 5, B: 7}
Total count: 5 + 7 = 12
This works because:
- Each node only increments its own component
- Merge is commutative: merge(A,B) = merge(B,A)
- Merge is idempotent: merge(A,A) = A
- Merge is associative: merge(A,merge(B,C)) = merge(merge(A,B),C)
Common CRDT Types:
| CRDT | Use Case | How It Works |
|---|---|---|
| G-Counter | Likes, views | Each node tracks own increments, merge = max |
| PN-Counter | Balance, inventory | Two G-Counters (positive, negative) |
| LWW-Register | Last-write-wins cell | Timestamp determines winner |
| OR-Set | Sets with add/remove | Each element tagged with unique ID |
| RGA | Collaborative text | Characters have position IDs, never deleted |
| Automerge | JSON documents | Combines multiple CRDTs for complex structures |
CRDTs guarantee conflict-free merge but don't guarantee user intent. If two users both delete the same paragraph and add different replacements, both additions survive even though the intent was replacement. This 'zombie data' problem requires careful UX design to handle gracefully.
The best conflict is one that never happens. Production systems employ various strategies to prevent conflicts before they occur, reducing the need for complex resolution.
| Lock Type | Granularity | Compatibility | Deadlock Risk |
|---|---|---|---|
| Exclusive (Write) | Entire file | Blocks all other writers | Possible if nested locks |
| Shared (Read) | Entire file | Multiple readers OK | None |
| Section Lock | Paragraph/cell | Other sections editable | Low |
| Intent Lock | Hierarchical | Signals upcoming access | Medium |
| Optimistic Lock | Conceptual | Check at commit time | None (but conflicts) |
Lock-Free Optimistic Approach (Most Common in Cloud):
Cloud storage systems typically don't use locks because:
Instead, they use optimistic concurrency:
1. User A starts editing (no lock acquired)
2. User B also starts editing (no lock acquired)
3. User A saves → succeeds (becomes revision 5)
4. User B saves with parent=4 → CONFLICT detected
(server has revision 5, not 4)
5. User B must resolve conflict before saving
This approach maximizes concurrency at the cost of occasional conflicts, which is the right trade-off for most collaboration scenarios.
When locks are necessary, use leases (time-limited locks) instead of permanent locks. A lease automatically expires if not renewed, preventing the 'orphaned lock' problem when a client crashes. Typical lease duration: 30-60 seconds, renewed every 10-20 seconds during active editing.
Real-world systems encounter edge cases that naive implementations handle poorly. Let's examine common edge cases and their solutions:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
// Edge case handlers for file system conflictsclass EdgeCaseHandler { // Delete vs Edit: User A deletes, User B edits handleDeleteVsEdit( deleteOp: DeleteOperation, editOp: EditOperation ): Resolution { // Never lose edits - restore file with new edits return { action: 'restore_and_apply', steps: [ { op: 'create', path: editOp.path, content: editOp.newContent }, { op: 'notify', user: deleteOp.userId, message: `${editOp.userEmail} edited ${editOp.path} ` + `which you deleted. File has been restored.` } ] }; } // Rename to same name by different users handleRenameNameConflict( rename1: RenameOperation, rename2: RenameOperation ): Resolution { // Both trying to rename to 'report.docx' // First one wins, second gets deduplicated name const winner = rename1.timestamp < rename2.timestamp ? rename1 : rename2; const loser = winner === rename1 ? rename2 : rename1; return { action: 'deduplicate', steps: [ { op: 'rename', from: winner.oldPath, to: winner.newPath }, { op: 'rename', from: loser.oldPath, to: this.deduplicateName(loser.newPath) // 'report (1).docx' } ] }; } // Folder move creating cycle handleMoveCycle( moveA: MoveOperation, // X into Y moveB: MoveOperation // Y into X ): Resolution { // Detect cycle: if we apply both, neither can be root // Resolution: Apply first, reject second with explanation const first = moveA.timestamp < moveB.timestamp ? moveA : moveB; const second = first === moveA ? moveB : moveA; return { action: 'partial_apply', steps: [ { op: 'move', operation: first }, { op: 'reject', operation: second, reason: 'Would create folder cycle' }, { op: 'notify', user: second.userId, message: `Could not move ${second.sourcePath} - would create ` + `circular structure.` } ] }; } // Cross-platform case sensitivity handleCaseSensitivityConflict( file1: string, // 'README.md' (created on Linux) file2: string // 'readme.md' (created on Windows) ): Resolution { // These are same file on Windows/macOS, different on Linux // Auto-rename one to avoid conflict on case-insensitive systems return { action: 'auto_rename', steps: [ { op: 'rename', from: file2, to: this.addSuffix(file2, '-1'), // 'readme-1.md' platform: 'case_insensitive' }, { op: 'notify', broadcast: true, message: `Renamed '${file2}' to avoid conflict with '${file1}' ` + `on case-insensitive systems.` } ] }; }}File names with emoji or special Unicode characters can cause unexpected conflicts. Unicode normalization (NFC vs NFD) means 'café' can be encoded differently by different systems. macOS uses NFD, most other systems use NFC. Always normalize file names to a consistent form (typically NFC) on the server.
Technical conflict resolution is only half the battle. Users must understand what happened and how to resolve it. Poor UX turns minor conflicts into major user frustration.
| Provider | Detection | Notification | Resolution |
|---|---|---|---|
| Dropbox | Server-side on upload | System notification + badge | Both files visible, manual merge |
| Google Drive | Real-time (OT) | In-editor banner | Auto-merge or fork document |
| OneDrive | Server-side on sync | Activity center | Keep both + visual diff tool |
| iCloud | Device sync | Finder displays both | Choose version to keep |
| Git | Merge/pull time | CLI output | Edit conflict markers, commit |
Users who frequently see conflict copies often start ignoring them—they accumulate, take up space, and become noise. Combat this with: (1) Conflict folder that groups all conflicts, (2) Age-based cleanup prompts, (3) Aggressive prevention through real-time collaboration features, (4) Analytics to identify frequently-conflicting files that should be collaboration docs.
Conflict resolution is one of the most challenging aspects of distributed file systems. Let's consolidate the key insights:
What's Next:
With synchronization and conflict resolution covered, the next page explores Chunked Uploads—how systems handle large file uploads reliably. We'll cover resumable uploads, parallel chunking, and the protocols that enable multi-gigabyte transfers over unreliable networks.
You now understand conflict detection, resolution strategies, and the advanced techniques (OT, CRDTs) that enable real-time collaboration. The key principle is clear: never lose user data, and make resolution as painless as possible. Next, we tackle the challenge of reliable large file uploads.