Skip to main content

Sync Conflict Resolution

Sync conflict resolution addresses competing changes to the same data record when offline systems reconnect. Field systems operating without continuous connectivity accumulate local modifications that must reconcile with changes made elsewhere during the disconnection period. This task covers conflict identification, resolution strategy selection, manual intervention procedures, and verification that resolved data maintains integrity.

Prerequisites

RequirementDetail
AccessAdministrator rights on synchronisation platform; write access to conflict queue
PermissionsData modification authority for affected record types; escalation path for protected data
ToolsPlatform CLI or admin console; database query access for complex conflicts
InformationConflict notification with record identifiers; user contact details for consultation
Time5-15 minutes per routine conflict; 30-60 minutes for complex multi-field disputes

Verify you have access to the conflict management interface before beginning. For CouchDB-based systems:

Terminal window
curl -u admin:password https://sync.example.org/_conflicts
# Expected: JSON array of document IDs with conflicts
# If 401 Unauthorized: verify credentials and admin role assignment

For ODK Central or similar platforms, confirm your account has the Project Manager role or higher for affected projects.

You need authority to determine which data version prevails when automated rules cannot resolve the conflict. Establish this authority before beginning: either documented delegation from the data owner, or direct assignment as the responsible data steward for the affected dataset.

Procedure

Identifying conflicts

  1. Access the conflict queue through your synchronisation platform’s administrative interface. The location varies by platform:

    CouchDB/PouchDB systems:

Terminal window
# List all documents with conflicts
curl -u admin:password \
'https://sync.example.org/fielddata/_all_docs?conflicts=true' \
| jq '.rows[] | select(.doc._conflicts)'

ODK Central: Navigate to the project, select the form, and check the Submissions tab for the conflict indicator (yellow warning icon). Conflicts appear when the same submission is edited on multiple devices before sync completes.

Microsoft Dataverse (offline-enabled): Open the Power Platform admin center, select the environment, navigate to Data > Tables, and filter for records with sync errors.

Custom sync implementations: Query the conflict log table directly:

SELECT conflict_id, record_type, record_id,
local_timestamp, remote_timestamp,
local_user, remote_user
FROM sync_conflicts
WHERE resolution_status = 'pending'
ORDER BY created_at DESC;
  1. Review each conflict record to understand what changed. Extract both versions for comparison:
Terminal window
# CouchDB: retrieve document with all conflicting revisions
curl -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?conflicts=true&revs=true'

The response includes _conflicts array listing revision IDs that conflict with the current winner. Retrieve each conflicting revision:

Terminal window
curl -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?rev=CONFLICTING_REV_ID'
  1. Document the conflict details before making changes. Record in your conflict log:

    • Record identifier and type
    • Timestamps of both versions
    • Users who made each change
    • Fields that differ between versions
    • Business context (if known) explaining why both changes occurred

Classifying conflict types

Conflicts fall into distinct categories requiring different resolution approaches. Examine the competing changes and classify the conflict before selecting a resolution strategy.

Last-write-wins candidates occur when changes affect different fields within the same record, or when one version contains strictly more recent information that supersedes the other. A field worker updating a beneficiary’s phone number while headquarters updates the same beneficiary’s programme enrollment status presents no true conflict because the changes are orthogonal.

Merge candidates arise when both versions contain valid data that should be combined. Two field workers each adding different household members to a family registration record during the same offline period exemplifies a merge scenario. Neither version is complete; the correct resolution combines both additions.

Authority-based resolution applies when organisational hierarchy or data ownership determines precedence. Changes to financial data from the finance system override changes from field collection systems. Updates from case managers override updates from data entry staff for case records.

Manual arbitration becomes necessary when competing changes represent genuine disagreement requiring human judgment. Two assessments of the same household reaching different conclusions about vulnerability status cannot be resolved through automated rules.

+------------------+
| Conflict |
| Detected |
+--------+---------+
|
+--------v---------+
| Same fields |
| modified? |
+--------+---------+
|
+-------------------+-------------------+
| |
| No | Yes
v v
+--------+---------+ +---------+--------+
| AUTO-MERGE | | Changes |
| Combine changes | | compatible? |
| from both | +---------+--------+
+------------------+ |
+---------------+---------------+
| |
| Yes | No
v v
+--------+---------+ +--------+---------+
| Check authority | | MANUAL |
| rules | | ARBITRATION |
+--------+---------+ | Human decision |
| | required |
+-------+-------+ +------------------+
| |
| Match | No match
v v
+--------+------+ +-----+----------+
| AUTHORITY | | MANUAL |
| Source with | | ARBITRATION |
| precedence | | Consult users |
| wins | | or data owner |
+---------------+ +----------------+

Figure 1: Conflict classification decision tree

Automated resolution configuration

Configure automated resolution rules before conflicts occur to reduce manual intervention. These rules execute during sync processing and resolve predictable conflict patterns without human involvement.

  1. Define field-level merge rules for records that support combining changes. In CouchDB, implement a conflict resolution function in your sync gateway configuration:
sync-gateway-config.json
{
"databases": {
"fielddata": {
"conflict_resolution_type": "custom",
"custom_conflict_resolver": `
function(conflict) {
var dominated = conflict.LocalDocument;
var dominant = conflict.RemoteDocument;
// Merge strategy: combine array fields
if (dominated.household_members && dominant.household_members) {
var combined = dominant.household_members.slice();
dominated.household_members.forEach(function(member) {
var exists = combined.some(function(m) {
return m.id === member.id;
});
if (!exists) {
combined.push(member);
}
});
dominant.household_members = combined;
}
// Last-write-wins for scalar fields
if (dominated._updated_at > dominant._updated_at) {
dominant.phone_number = dominated.phone_number;
dominant.address = dominated.address;
}
return dominant;
}
`
}
}
}
  1. Configure authority-based precedence for system integrations. When data flows from multiple sources with defined hierarchy:
// Authority ranking (higher number = higher authority)
const SOURCE_AUTHORITY = {
'finance-system': 100,
'hr-system': 90,
'case-management': 80,
'field-collection': 50,
'beneficiary-portal': 30
};
function resolveByAuthority(conflict) {
var localAuthority = SOURCE_AUTHORITY[conflict.local.source] || 0;
var remoteAuthority = SOURCE_AUTHORITY[conflict.remote.source] || 0;
if (localAuthority > remoteAuthority) {
return conflict.local;
} else if (remoteAuthority > localAuthority) {
return conflict.remote;
} else {
// Equal authority: fall back to timestamp
return conflict.local.updated_at > conflict.remote.updated_at
? conflict.local
: conflict.remote;
}
}
  1. Set thresholds for automatic resolution. Not all conflicts should resolve automatically. Configure limits:
conflict-resolution-config.yaml
auto_resolution:
enabled: true
max_field_differences: 5
max_age_hours: 72
excluded_record_types:
- financial_transaction
- safeguarding_case
- consent_record
excluded_fields:
- signature
- final_assessment
- approval_status
require_audit_trail: true

This configuration allows automatic resolution when fewer than 5 fields differ and the conflict is less than 72 hours old, but excludes sensitive record types and fields requiring human review.

  1. Test automated rules with synthetic conflicts before deploying to production:
Terminal window
# Create test conflict
curl -X PUT -u admin:password \
-H "Content-Type: application/json" \
-d '{"_id":"test-conflict-001","name":"Test A","status":"active"}' \
'https://sync.example.org/fielddata/test-conflict-001'
# Retrieve revision ID
REV=$(curl -s -u admin:password \
'https://sync.example.org/fielddata/test-conflict-001' \
| jq -r '._rev')
# Create conflicting update (simulating offline device)
curl -X PUT -u admin:password \
-H "Content-Type: application/json" \
-d "{\"_id\":\"test-conflict-001\",\"_rev\":\"$REV\",\"name\":\"Test B\",\"status\":\"inactive\",\"new_field\":true}" \
'https://sync.example.org/fielddata/test-conflict-001?new_edits=false'
# Verify conflict exists
curl -s -u admin:password \
'https://sync.example.org/fielddata/test-conflict-001?conflicts=true' \
| jq '._conflicts'
# Trigger resolution and verify result

Manual resolution procedures

When automated rules cannot resolve a conflict, or when the conflict involves protected data types, manual intervention is required.

  1. Retrieve complete details of both conflicting versions. Create a comparison document:
Terminal window
# Export both versions to files for comparison
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID' > version_current.json
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?rev=CONFLICT_REV' > version_conflict.json
# Generate diff
diff -u version_current.json version_conflict.json > conflict_diff.txt

For structured comparison, use a JSON diff tool:

Terminal window
jq -S '.' version_current.json > current_sorted.json
jq -S '.' version_conflict.json > conflict_sorted.json
diff current_sorted.json conflict_sorted.json
  1. Contact the users who made each change when the correct resolution is not clear from the data alone. The conflict record identifies users through the modified_by or equivalent field:
-- Find user contact details for conflict consultation
SELECT u.email, u.name, u.phone, u.office_location
FROM users u
JOIN sync_conflicts c ON c.local_user = u.id OR c.remote_user = u.id
WHERE c.conflict_id = 'CONFLICT_ID';

When contacting users:

  • Describe the specific fields in conflict
  • Ask for context about why they made the change
  • Request their assessment of which version is correct
  • Document their response for the audit trail
  1. Determine the resolution based on conflict type and gathered information. For each conflicting field, decide whether to:

    • Keep the local value (from the device that synced first)
    • Keep the remote value (from the device that synced second)
    • Merge values (for array or composite fields)
    • Create a new value incorporating information from both
    • Escalate to data owner if you cannot determine correct resolution
  2. Apply the resolution by updating the document and removing conflict markers:

    CouchDB: resolve by deleting losing revision

Terminal window
# Keep current winner, delete conflicting revision
curl -X DELETE -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?rev=CONFLICT_REV'

CouchDB: resolve by replacing winner with merged document

Terminal window
# Get current revision
CURRENT_REV=$(curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID' | jq -r '._rev')
# Update with merged data
curl -X PUT -u admin:password \
-H "Content-Type: application/json" \
-d '{
"_id": "RECORD_ID",
"_rev": "'"$CURRENT_REV"'",
"field1": "value_from_local",
"field2": "value_from_remote",
"merged_array": ["item_from_local", "item_from_remote"],
"_resolution_note": "Merged by admin on 2024-11-16, ticket #1234"
}' \
'https://sync.example.org/fielddata/RECORD_ID'
# Delete the losing revision
curl -X DELETE -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?rev=CONFLICT_REV'

ODK Central: resolve through submission edit

Access the submission through the web interface, select Edit, and modify fields to the correct values. The edit creates a new version that supersedes both conflicting versions.

  1. Add resolution metadata to maintain audit trail:
{
"_resolution": {
"resolved_at": "2024-11-16T14:30:00Z",
"resolved_by": "admin@example.org",
"resolution_type": "manual_merge",
"local_version_kept": ["field1", "field3"],
"remote_version_kept": ["field2"],
"merged_fields": ["array_field"],
"ticket_reference": "SUPPORT-1234",
"notes": "Consulted both field workers; merged household member lists"
}
}

Resolution documentation

Every resolved conflict requires documentation for audit purposes and to inform future prevention efforts.

  1. Update the conflict log with resolution details:
UPDATE sync_conflicts
SET resolution_status = 'resolved',
resolution_type = 'manual_merge',
resolved_by = 'admin@example.org',
resolved_at = NOW(),
resolution_notes = 'Merged household members from both versions after consultation with field workers',
fields_from_local = ARRAY['registration_date', 'collector_id'],
fields_from_remote = ARRAY['phone_number', 'address'],
merged_fields = ARRAY['household_members']
WHERE conflict_id = 'CONFLICT_ID';
  1. Record the data lineage for merged records. When a resolution combines data from multiple sources, downstream systems need to understand the provenance:
{
"_data_lineage": {
"sources": [
{
"version": "3-abc123",
"timestamp": "2024-11-15T09:00:00Z",
"device": "tablet-field-001",
"user": "fieldworker1@example.org",
"fields_contributed": ["registration_date", "household_members[0-2]"]
},
{
"version": "3-def456",
"timestamp": "2024-11-15T11:30:00Z",
"device": "tablet-field-002",
"user": "fieldworker2@example.org",
"fields_contributed": ["phone_number", "household_members[3-4]"]
}
],
"merge_timestamp": "2024-11-16T14:30:00Z",
"merge_authority": "admin@example.org"
}
}
  1. Notify affected users that the conflict has been resolved:
Subject: Sync conflict resolved - Record [RECORD_ID]
A synchronisation conflict affecting record [RECORD_ID] has been resolved.
Your changes: [summary of their changes]
Other changes: [summary of competing changes]
Resolution: [what was kept/merged]
If you believe this resolution is incorrect, contact [support contact]
within 48 hours referencing ticket [TICKET_ID].

Escalation procedures

Some conflicts require escalation to data owners, programme managers, or technical specialists.

  1. Identify escalation triggers. Escalate when:

    • Conflict involves protected data types (safeguarding, financial, consent)
    • Conflicting users disagree on correct resolution after consultation
    • Resolution would result in data loss exceeding retention requirements
    • Conflict pattern suggests systematic issue requiring process change
    • You lack authority to determine precedence between conflicting sources
  2. Prepare escalation documentation:

CONFLICT ESCALATION REQUEST
Conflict ID: [ID]
Record Type: [type]
Record ID: [ID]
Conflicting Versions:
- Version A: [summary, user, timestamp]
- Version B: [summary, user, timestamp]
Fields in Conflict:
[list of fields with both values]
Consultation Performed:
[summary of user contacts and responses]
Recommended Resolution: [your recommendation if you have one]
Reason for Escalation: [why you cannot resolve]
Requested Decision: [specific question for escalation recipient]
  1. Route escalation to appropriate authority:

    Conflict TypeEscalation Path
    Programme dataProgramme Manager
    Financial dataFinance Manager
    Safeguarding/protectionSafeguarding Lead
    HR/personnel dataHR Manager
    Cross-programmeData Governance Lead
    Technical/systematicIT Manager
  2. Implement escalation decision and document the authority for resolution:

UPDATE sync_conflicts
SET resolution_status = 'resolved',
resolution_type = 'escalation',
escalated_to = 'programme.manager@example.org',
escalation_decision = 'Keep Version A per programme manager decision',
escalation_reference = 'EMAIL-2024-11-16-001'
WHERE conflict_id = 'CONFLICT_ID';

Verification

After resolving conflicts, verify data integrity and confirm the resolution propagates correctly.

Confirm conflict cleared from queue:

Terminal window
# Verify no conflicts remain on the record
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?conflicts=true' \
| jq '._conflicts // "no conflicts"'
# Expected: "no conflicts" or null
# Verify record removed from conflict tracking
curl -s -u admin:password \
'https://sync.example.org/fielddata/_all_docs?conflicts=true' \
| jq '.rows[] | select(.id == "RECORD_ID")'
# Expected: no output (record not in conflict list)

Validate resolved data integrity:

Terminal window
# Check required fields present
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID' \
| jq 'has("required_field1") and has("required_field2")'
# Expected: true
# Verify data types
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID' \
| jq '.numeric_field | type'
# Expected: "number"

Confirm sync propagation to downstream systems:

Terminal window
# Check replication status
curl -s -u admin:password \
'https://sync.example.org/_active_tasks' \
| jq '.[] | select(.type == "replication")'
# Verify record appears on target
curl -s -u readonly:password \
'https://reporting.example.org/fielddata/RECORD_ID' \
| jq '._rev'
# Should match the resolved revision

Verify audit trail completeness:

SELECT conflict_id, record_id, resolution_type, resolved_by,
resolved_at, resolution_notes
FROM sync_conflicts
WHERE record_id = 'RECORD_ID'
AND resolution_status = 'resolved'
ORDER BY resolved_at DESC
LIMIT 1;

The query should return the resolution record with all fields populated.

Troubleshooting

SymptomCauseResolution
Conflict reappears after resolutionOffline device syncs with old conflicting revisionIdentify device still holding conflict; force sync or clear local data on device
”Conflict not found” when attempting resolutionAnother administrator resolved the conflict; or conflict ID incorrectRefresh conflict list; verify conflict ID; check resolution history
Resolution creates new conflictMerge introduced data that conflicts with third versionRetrieve all revisions including the new conflict; resolve comprehensively
Cannot delete conflicting revision (403 Forbidden)Insufficient permissions on databaseVerify admin role; check database security rules for conflict resolution permissions
Automated resolution produces incorrect resultRule logic error or edge case not coveredDisable automated resolution temporarily; resolve manually; update rule logic
User reports data loss after resolutionWrong version selected as winner; or merge incompleteRetrieve historical revisions from compaction-safe backup; restore lost data; re-resolve
Conflict involves deleted documentOne version is deletion, other is updateDecide whether delete or update takes precedence based on business rules; apply resolution
Bulk conflicts after connectivity restorationMany devices syncing simultaneously with overlapping changesPrioritise by data criticality; consider temporary sync pause; resolve systematically
Resolution metadata not savedDocument update succeeded but metadata strippedCheck for document validation rules removing unknown fields; add _resolution to allowed fields
Downstream system shows different data than sourceReplication lag or transformation issueForce replication; check transformation rules; verify downstream system received update
Cannot identify users who made changesmodified_by field not populatedCheck sync configuration captures user attribution; may need to resolve without user consultation
Conflict affects calculated/derived fieldBoth versions have stale calculationsRecalculate derived fields after resolving base data; do not merge calculated values

Persistent conflict loops

When the same record repeatedly enters conflict state:

Terminal window
# Identify conflict frequency for a record
SELECT record_id, COUNT(*) as conflict_count,
array_agg(DISTINCT local_user) as users_involved
FROM sync_conflicts
WHERE record_id = 'PROBLEMATIC_RECORD_ID'
GROUP BY record_id;

If conflict count exceeds 3 within 30 days, investigate:

  1. Check if multiple users are assigned to the same data without coordination
  2. Verify sync frequency is adequate for the update rate
  3. Consider record locking during active editing sessions
  4. Review whether record should be split to reduce contention

Data loss recovery

If resolution accidentally discarded needed data:

Terminal window
# CouchDB: retrieve all historical revisions
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?revs_info=true' \
| jq '.["_revs_info"][] | select(.status == "available")'
# Retrieve specific historical revision
curl -s -u admin:password \
'https://sync.example.org/fielddata/RECORD_ID?rev=HISTORICAL_REV'

If revisions have been compacted, restore from backup:

Terminal window
# Restore single document from backup
couchdb-restore --source backup-2024-11-15.tar.gz \
--database fielddata \
--document RECORD_ID \
--target https://sync.example.org/fielddata_recovery/

Prevention strategies

Reduce conflict frequency through configuration and process improvements:

Increase sync frequency for high-contention records. Default hourly sync may be insufficient for actively edited records:

// Configure priority sync for high-activity records
{
"sync_priorities": {
"high": {
"record_types": ["active_case", "daily_distribution"],
"sync_interval_minutes": 5,
"sync_on_change": true
},
"normal": {
"sync_interval_minutes": 60
}
}
}

Implement record locking for exclusive editing scenarios:

// Request edit lock before modifying
async function acquireEditLock(recordId, userId) {
const lockDoc = {
_id: `lock:${recordId}`,
record_id: recordId,
locked_by: userId,
locked_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30 min
};
try {
await db.put(lockDoc);
return true;
} catch (err) {
if (err.status === 409) {
// Lock exists; check if expired
const existing = await db.get(`lock:${recordId}`);
if (new Date(existing.expires_at) < new Date()) {
// Expired; take over lock
lockDoc._rev = existing._rev;
await db.put(lockDoc);
return true;
}
return false; // Active lock by another user
}
throw err;
}
}

Partition data by responsibility to reduce overlap. When feasible, assign geographic or functional segments to specific users:

-- Example: assign villages to specific data collectors
UPDATE data_collection_assignments
SET assigned_collector = 'collector_a@example.org'
WHERE village_id IN ('village_001', 'village_002', 'village_003');

Design conflict-resistant data models using append-only patterns for high-contention fields:

{
"_id": "household-001",
"base_data": {
"head_of_household": "Name",
"location": "Village A"
},
"members_log": [
{"action": "add", "member": {...}, "by": "user1", "at": "2024-01-01"},
{"action": "add", "member": {...}, "by": "user2", "at": "2024-01-02"},
{"action": "remove", "member_id": "m1", "by": "user1", "at": "2024-01-03"}
]
}

This pattern converts conflicting updates into non-conflicting appends. Derive current state by replaying the log.

See also