Documentation Index Fetch the complete documentation index at: https://mintlify.com/ably/docs/llms.txt
Use this file to discover all available pages before exploring further.
Understanding Conflicts
In distributed systems with realtime synchronization, conflicts occur when multiple clients or processes attempt to modify the same data concurrently. LiveSync provides mechanisms to detect, prevent, and resolve these conflicts.
Types of Conflicts
1. Concurrent Updates
Multiple users modifying the same resource simultaneously:
// User A and User B both edit the same document
// User A: Changes title to "Project Alpha"
// User B: Changes title to "Project Beta"
// Conflict: Which title should be kept?
2. Optimistic Update Conflicts
Client-side optimistic updates that fail server validation:
// Client optimistically updates task status
model . optimistic ({
mutationId: 'update-123' ,
name: 'task.updated' ,
data: { status: 'completed' }
});
// Server rejects due to business logic
// (e.g., task has unmet dependencies)
3. Ordering Conflicts
Events arriving out of order due to network conditions:
// Events published: Delete -> Update
// Events received: Update -> Delete
// Conflict: Processing order matters
4. State Divergence
Client state drifting from server state due to missed updates:
// Client disconnects
// Server processes updates: A, B, C
// Client reconnects but only receives: A, C
// Conflict: Missing event B
Conflict Resolution Strategies
Last-Write-Wins (LWW)
The most recent update overwrites previous changes:
function merge ( state , event ) {
const existingItem = state . items . find ( i => i . id === event . data . id );
if ( ! existingItem ) {
// New item, add it
state . items . push ( event . data );
} else {
// Compare timestamps
if ( event . data . updatedAt > existingItem . updatedAt ) {
// Newer update wins
Object . assign ( existingItem , event . data );
}
// Otherwise, ignore older update
}
return state ;
}
Pros:
Simple to implement
No user intervention needed
Works well for single-field updates
Cons:
Can lose data
Not suitable for complex objects
No merge of concurrent changes
Field-Level Merging
Merge changes at the field level rather than object level:
function merge ( state , event ) {
const item = state . items . find ( i => i . id === event . data . id );
if ( ! item ) {
state . items . push ( event . data );
return state ;
}
// Merge field by field
Object . keys ( event . data . changes ). forEach ( field => {
const newValue = event . data . changes [ field ];
const currentValue = item [ field ];
// Compare timestamps per field
if ( ! item . _timestamps ?.[ field ] ||
event . data . timestamp > item . _timestamps [ field ]) {
item [ field ] = newValue ;
item . _timestamps = item . _timestamps || {};
item . _timestamps [ field ] = event . data . timestamp ;
}
});
return state ;
}
Example:
// User A updates: { title: "New Title", timestamp: 100 }
// User B updates: { status: "done", timestamp: 101 }
// Result: { title: "New Title", status: "done" }
Pros:
Preserves more changes
Better for multi-field objects
Reduces data loss
Cons:
More complex implementation
Requires field-level tracking
May create inconsistent states
Transform operations to maintain consistency:
// Example: Collaborative text editing
function transformOperations ( op1 , op2 ) {
// If op1 inserts "Hello" at position 0
// And op2 inserts "World" at position 0
// Transform op2 to insert at position 5 (after "Hello")
if ( op1 . type === 'insert' && op2 . type === 'insert' ) {
if ( op2 . position >= op1 . position ) {
op2 . position += op1 . text . length ;
}
}
return [ op1 , op2 ];
}
function merge ( state , event ) {
const transformed = transformAgainstHistory (
event . data . operations ,
state . operationHistory
);
applyOperations ( state , transformed );
state . operationHistory . push ( transformed );
return state ;
}
Pros:
Ideal for collaborative editing
Strong consistency guarantees
Preserves user intent
Cons:
Complex implementation
Requires operation history
Higher computational cost
Conflict-Free Replicated Data Types (CRDTs)
Use data structures that automatically resolve conflicts:
import { LWWMap } from 'yjs' ; // Example CRDT library
function merge ( state , event ) {
// Use CRDT for automatic conflict resolution
const crdt = new LWWMap ( state . data );
event . data . changes . forEach ( change => {
crdt . set ( change . key , change . value , change . timestamp );
});
return {
... state ,
data: crdt . toJSON ()
};
}
Common CRDT Types:
LWW-Element-Set : Last-write-wins for sets
OR-Set : Observed-remove set
G-Counter : Grow-only counter
PN-Counter : Positive-negative counter
Pros:
Automatic conflict resolution
Strong consistency
No coordination needed
Cons:
Limited data structure types
Memory overhead
Learning curve
Server-Authoritative Resolution
Let the server be the single source of truth:
// Client side: Only display confirmed updates
model . subscribe (
( err , state ) => {
// Only confirmed changes from server
updateUI ( state );
},
{ optimistic: false } // Disable optimistic updates
);
// Server side: Validate and resolve conflicts
app . post ( '/api/tasks/:id' , async ( req , res ) => {
const { id } = req . params ;
const { changes , expectedVersion } = req . body ;
await db . transaction ( async ( tx ) => {
// Lock the row for update
const task = await tx . query (
'SELECT * FROM tasks WHERE id = $1 FOR UPDATE' ,
[ id ]
);
// Check version
if ( task . version !== expectedVersion ) {
throw new Error ( 'Conflict: Task was modified' );
}
// Apply changes and increment version
await tx . query ( `
UPDATE tasks
SET status = $1, version = version + 1
WHERE id = $2
` , [ changes . status , id ]);
// Publish to outbox
await tx . query ( `
INSERT INTO outbox (channel, name, data)
VALUES ($1, $2, $3)
` , [ `tasks: ${ id } ` , 'task.updated' , JSON . stringify ( changes )]);
});
res . json ({ success: true });
});
Pros:
Simple client logic
Server controls business rules
Clear consistency model
Cons:
Higher latency for updates
No optimistic updates
Server bottleneck
Optimistic Update Patterns
Optimistic with Rollback
Apply updates immediately, rollback on failure:
async function updateTask ( taskId , status ) {
const mutationId = crypto . randomUUID ();
// Apply optimistically
const [ confirmation , cancel ] = await model . optimistic ({
mutationId ,
name: 'task.updated' ,
data: { id: taskId , status }
});
try {
// Send to server
await fetch ( `/api/tasks/ ${ taskId } ` , {
method: 'PATCH' ,
body: JSON . stringify ({ mutationId , status })
});
// Wait for confirmation
await confirmation ;
} catch ( error ) {
// Rollback on error
cancel ();
// Notify user
showError ( 'Failed to update task' );
}
}
Optimistic with Reconciliation
Merge optimistic state with confirmed state:
function merge ( state , event ) {
if ( event . type === 'confirmed' ) {
// Server confirmed update
const optimisticEvent = state . pendingEvents . find (
e => e . mutationId === event . mutationId
);
if ( optimisticEvent ) {
// Compare optimistic vs confirmed
if ( JSON . stringify ( optimisticEvent . data ) !==
JSON . stringify ( event . data )) {
// Server made changes, use server version
console . log ( 'Server modified update:' , event . data );
}
// Remove from pending
state . pendingEvents = state . pendingEvents . filter (
e => e . mutationId !== event . mutationId
);
}
// Apply confirmed update
applyUpdate ( state , event . data );
} else if ( event . type === 'optimistic' ) {
// Track pending
state . pendingEvents . push ( event );
applyUpdate ( state , event . data );
}
return state ;
}
Optimistic with Retry
Retry failed optimistic updates:
async function updateWithRetry ( taskId , changes , maxRetries = 3 ) {
let attempt = 0 ;
while ( attempt < maxRetries ) {
try {
const mutationId = crypto . randomUUID ();
const [ confirmation , cancel ] = await model . optimistic ({
mutationId ,
name: 'task.updated' ,
data: { id: taskId , ... changes }
});
await fetch ( `/api/tasks/ ${ taskId } ` , {
method: 'PATCH' ,
body: JSON . stringify ({ mutationId , ... changes })
});
await confirmation ;
return ; // Success
} catch ( error ) {
attempt ++ ;
if ( attempt >= maxRetries ) {
throw new Error ( 'Max retries exceeded' );
}
// Exponential backoff
await new Promise ( resolve =>
setTimeout ( resolve , Math . pow ( 2 , attempt ) * 1000 )
);
}
}
}
Version Control Strategies
Optimistic Locking
Use version numbers to detect conflicts:
// Client side
async function updateDocument ( docId , changes , currentVersion ) {
const response = await fetch ( `/api/documents/ ${ docId } ` , {
method: 'PATCH' ,
body: JSON . stringify ({
changes ,
expectedVersion: currentVersion
})
});
if ( response . status === 409 ) {
// Conflict detected
const { latestVersion , latestData } = await response . json ();
// Show conflict resolution UI
showConflictDialog ({
local: changes ,
remote: latestData ,
onResolve : ( resolved ) => {
updateDocument ( docId , resolved , latestVersion );
}
});
}
}
// Server side
app . patch ( '/api/documents/:id' , async ( req , res ) => {
const { id } = req . params ;
const { changes , expectedVersion } = req . body ;
await db . transaction ( async ( tx ) => {
const doc = await tx . query (
'SELECT * FROM documents WHERE id = $1 FOR UPDATE' ,
[ id ]
);
if ( doc . version !== expectedVersion ) {
// Version mismatch - conflict!
return res . status ( 409 ). json ({
error: 'Conflict' ,
latestVersion: doc . version ,
latestData: doc . data
});
}
// Update with new version
await tx . query ( `
UPDATE documents
SET data = $1, version = $2
WHERE id = $3
` , [ changes , expectedVersion + 1 , id ]);
// Publish to outbox
await tx . query ( `
INSERT INTO outbox (channel, name, data)
VALUES ($1, $2, $3)
` , [ `docs: ${ id } ` , 'doc.updated' , JSON . stringify ( changes )]);
});
res . json ({ success: true });
});
Vector Clocks
Track causality across distributed updates:
class VectorClock {
constructor () {
this . clock = {};
}
increment ( nodeId ) {
this . clock [ nodeId ] = ( this . clock [ nodeId ] || 0 ) + 1 ;
}
merge ( other ) {
Object . keys ( other . clock ). forEach ( nodeId => {
this . clock [ nodeId ] = Math . max (
this . clock [ nodeId ] || 0 ,
other . clock [ nodeId ]
);
});
}
compare ( other ) {
let greater = false ;
let less = false ;
const allNodes = new Set ([
... Object . keys ( this . clock ),
... Object . keys ( other . clock )
]);
for ( const node of allNodes ) {
const thisValue = this . clock [ node ] || 0 ;
const otherValue = other . clock [ node ] || 0 ;
if ( thisValue > otherValue ) greater = true ;
if ( thisValue < otherValue ) less = true ;
}
if ( greater && ! less ) return 1 ; // this > other
if ( less && ! greater ) return - 1 ; // this < other
if ( greater && less ) return 0 ; // concurrent
return 2 ; // equal
}
}
function merge ( state , event ) {
const eventClock = new VectorClock ();
eventClock . clock = event . vectorClock ;
const stateClock = new VectorClock ();
stateClock . clock = state . vectorClock ;
const comparison = stateClock . compare ( eventClock );
if ( comparison === - 1 ) {
// Event is newer, apply it
applyUpdate ( state , event . data );
stateClock . merge ( eventClock );
state . vectorClock = stateClock . clock ;
} else if ( comparison === 0 ) {
// Concurrent updates - conflict!
handleConflict ( state , event );
}
// comparison === 1: Event is older, ignore
return state ;
}
Conflict Detection
Monitoring Conflicts
const conflictMetrics = {
total: 0 ,
resolved: 0 ,
manual: 0
};
function merge ( state , event ) {
const conflict = detectConflict ( state , event );
if ( conflict ) {
conflictMetrics . total ++ ;
// Log conflict for analysis
logConflict ({
type: conflict . type ,
resource: event . data . id ,
timestamp: Date . now (),
details: conflict . details
});
// Attempt automatic resolution
const resolved = resolveConflict ( state , event , conflict );
if ( resolved ) {
conflictMetrics . resolved ++ ;
return resolved ;
}
// Require manual resolution
conflictMetrics . manual ++ ;
showConflictUI ( state , event , conflict );
}
return state ;
}
function detectConflict ( state , event ) {
const item = state . items . find ( i => i . id === event . data . id );
if ( ! item ) return null ;
// Check for concurrent modifications
if ( event . data . version !== item . version + 1 ) {
return {
type: 'version-mismatch' ,
expected: item . version + 1 ,
received: event . data . version
};
}
// Check for pending optimistic updates
const pending = state . pendingEvents . find (
e => e . data . id === event . data . id
);
if ( pending ) {
return {
type: 'optimistic-conflict' ,
optimistic: pending . data ,
confirmed: event . data
};
}
return null ;
}
User Notification
function showConflictDialog ( conflict ) {
const dialog = {
title: 'Conflicting Changes Detected' ,
message: `This ${ conflict . resourceType } was modified by another user.` ,
options: [
{
label: 'Keep My Changes' ,
value: 'local' ,
description: 'Overwrite remote changes with yours'
},
{
label: 'Use Their Changes' ,
value: 'remote' ,
description: 'Discard your changes'
},
{
label: 'Merge Changes' ,
value: 'merge' ,
description: 'Combine both sets of changes'
},
{
label: 'Review Differences' ,
value: 'review' ,
description: 'See detailed comparison'
}
],
onResolve : ( choice ) => {
switch ( choice ) {
case 'local' :
forceUpdate ( conflict . local );
break ;
case 'remote' :
acceptRemote ( conflict . remote );
break ;
case 'merge' :
showMergeEditor ( conflict );
break ;
case 'review' :
showDiffViewer ( conflict );
break ;
}
}
};
showDialog ( dialog );
}
Best Practices
1. Choose the Right Strategy
// For simple data: Last-write-wins
const simpleData = {
status: 'active' ,
priority: 'high'
};
// For complex objects: Field-level merging
const complexData = {
title: 'Project' ,
description: 'Long description...' ,
metadata: { /* ... */ }
};
// For collaborative editing: Operational Transformation
const collaborativeText = {
content: 'Document content...' ,
operations: [ /* ... */ ]
};
2. Implement Proper Validation
app . patch ( '/api/resources/:id' , async ( req , res ) => {
const { id } = req . params ;
const { changes , mutationId } = req . body ;
try {
// Validate changes
const errors = validateChanges ( changes );
if ( errors . length > 0 ) {
// Reject optimistic update
await rejectMutation ( mutationId , errors );
return res . status ( 400 ). json ({ errors });
}
// Apply changes
await applyChanges ( id , changes , mutationId );
res . json ({ success: true });
} catch ( error ) {
await rejectMutation ( mutationId , [ error . message ]);
res . status ( 500 ). json ({ error: error . message });
}
});
async function rejectMutation ( mutationId , errors ) {
await db . query ( `
INSERT INTO outbox (mutation_id, channel, name, rejected, data)
VALUES ($1, $2, $3, true, $4)
` , [ mutationId , 'errors' , 'update.rejected' , JSON . stringify ( errors )]);
}
3. Test Conflict Scenarios
describe ( 'Conflict Resolution' , () => {
it ( 'should handle concurrent updates' , async () => {
const model = createModel ();
// Simulate concurrent updates
const update1 = { id: 1 , title: 'Title A' , timestamp: 100 };
const update2 = { id: 1 , title: 'Title B' , timestamp: 101 };
const state1 = merge ( model . state , { data: update1 });
const state2 = merge ( state1 , { data: update2 });
// Verify resolution
expect ( state2 . items [ 0 ]. title ). toBe ( 'Title B' ); // Later wins
});
it ( 'should rollback failed optimistic updates' , async () => {
const model = createModel ();
// Apply optimistic update
const [ confirmation , cancel ] = await model . optimistic ({
mutationId: 'test-123' ,
data: { status: 'done' }
});
// Simulate rejection
cancel ();
// Verify rollback
expect ( model . state . items [ 0 ]. status ). toBe ( 'pending' );
});
});
4. Monitor and Analyze
// Track conflict metrics
const metrics = {
conflicts: {
total: 0 ,
autoResolved: 0 ,
manualResolved: 0 ,
byType: {}
}
};
function recordConflict ( type , resolution ) {
metrics . conflicts . total ++ ;
metrics . conflicts . byType [ type ] =
( metrics . conflicts . byType [ type ] || 0 ) + 1 ;
if ( resolution === 'auto' ) {
metrics . conflicts . autoResolved ++ ;
} else if ( resolution === 'manual' ) {
metrics . conflicts . manualResolved ++ ;
}
// Send to analytics
analytics . track ( 'conflict' , { type , resolution });
}
5. Document Behavior
/**
* Merges incoming change events with existing state.
*
* Conflict Resolution Strategy:
* - Uses last-write-wins for simple field updates
* - Requires manual resolution for complex objects
* - Automatically rolls back rejected optimistic updates
*
* @param {Object} state - Current model state
* @param {Object} event - Incoming change event
* @returns {Object} Updated state
*/
function merge ( state , event ) {
// Implementation...
}
Next Steps
Database Sync Learn more about database synchronization patterns
Models SDK Explore the Models SDK for advanced conflict handling
Quickstart Build a realtime application with LiveSync