Skip to content

Handle Concurrency Conflicts

Deal with multiple users modifying the same data simultaneously

Event sourcing shines when multiple users try to modify the same data. Here’s how to handle it properly.

Two users load the same todo. Both try to update it. Without proper handling, one update overwrites the other.

DeltaBase uses optimistic concurrency control. When appending events, specify the expected version:

try {
await eventStore.appendToStream('todo-123', [updateEvent], {
expectedStreamVersion: 5 // Fail if stream has moved past version 5
});
} catch (error) {
if (error.status === 409) {
// Conflict! Handle it
console.log('Someone else modified this todo');
}
}

Simple but data loss-prone:

async function updateTodoLastWriterWins(id: string, updates: Partial<Todo>) {
const event = {
type: 'todo.updated',
data: { id, ...updates }
};
// No expected version - always wins
await eventStore.appendToStream(id, [event]);
}

Let the user know there’s a conflict:

async function updateTodoFailFast(id: string, updates: Partial<Todo>, expectedVersion: number) {
const event = {
type: 'todo.updated',
data: { id, ...updates }
};
try {
await eventStore.appendToStream(id, [event], { expectedStreamVersion });
return { success: true };
} catch (error) {
if (error.status === 409) {
return {
success: false,
error: 'Todo was modified by someone else. Please refresh and try again.'
};
}
throw error;
}
}

Detect conflicts and merge automatically:

async function updateTodoAutoMerge(id: string, updates: Partial<Todo>, expectedVersion: number) {
const event = {
type: 'todo.updated',
data: { id, ...updates }
};
try {
await eventStore.appendToStream(id, [event], { expectedStreamVersion });
return { success: true };
} catch (error) {
if (error.status === 409) {
// Get current state
const events = await eventStore.readStream(id);
const currentTodo = buildTodoFromEvents(events);
// Check if our changes conflict
const hasConflict = checkForConflicts(currentTodo, updates);
if (!hasConflict) {
// Safe to retry with latest version
const latestVersion = events.length - 1;
await eventStore.appendToStream(id, [event], {
expectedStreamVersion: latestVersion
});
return { success: true, merged: true };
}
return {
success: false,
error: 'Conflicting changes detected',
currentState: currentTodo
};
}
throw error;
}
}
function checkForConflicts(currentTodo: Todo, updates: Partial<Todo>): boolean {
// Example: Title changes conflict, but description changes don't
if (updates.title && currentTodo.title !== updates.title) {
return true;
}
return false;
}

Most sophisticated - merge based on original state:

async function updateTodoThreeWayMerge(
id: string,
updates: Partial<Todo>,
originalState: Todo
) {
// Get current state
const events = await eventStore.readStream(id);
const currentState = buildTodoFromEvents(events);
// Perform three-way merge
const mergedUpdates = threeWayMerge(originalState, currentState, updates);
if (mergedUpdates.conflicts.length > 0) {
return {
success: false,
conflicts: mergedUpdates.conflicts,
currentState
};
}
// Apply merged changes
const event = {
type: 'todo.updated',
data: { id, ...mergedUpdates.changes }
};
const latestVersion = events.length - 1;
await eventStore.appendToStream(id, [event], {
expectedStreamVersion: latestVersion
});
return { success: true };
}
function threeWayMerge(original: Todo, current: Todo, updates: Partial<Todo>) {
const changes: Partial<Todo> = {};
const conflicts: string[] = [];
// Check each field
for (const [key, value] of Object.entries(updates)) {
const originalValue = original[key];
const currentValue = current[key];
if (originalValue === currentValue) {
// No conflict - current hasn't changed
changes[key] = value;
} else if (currentValue === value) {
// No conflict - both changed to same value
changes[key] = value;
} else {
// Conflict - both changed to different values
conflicts.push(key);
}
}
return { changes, conflicts };
}

In your web app, track versions:

// When loading data
const todo = await api.getTodo('todo-123');
const version = todo._version; // Include version in your API response
// When updating
const result = await api.updateTodo('todo-123', {
title: 'New title'
}, version);
if (!result.success) {
// Show conflict resolution UI
showConflictDialog(result.error, result.currentState);
}
  1. Use expected versions for all mutations - Don’t let silent overwrites happen
  2. Show versions to users - Let them know when data is stale
  3. Implement refresh workflows - Easy way for users to get latest data
  4. Choose appropriate strategy - Consider your domain and user expectations
  5. Test conflict scenarios - Simulate concurrent updates in your tests

Remember: In event sourcing, conflicts are features, not bugs. They help you maintain data integrity.