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
| Requirement | Detail |
|---|---|
| Access | Administrator rights on synchronisation platform; write access to conflict queue |
| Permissions | Data modification authority for affected record types; escalation path for protected data |
| Tools | Platform CLI or admin console; database query access for complex conflicts |
| Information | Conflict notification with record identifiers; user contact details for consultation |
| Time | 5-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:
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 assignmentFor 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
Access the conflict queue through your synchronisation platform’s administrative interface. The location varies by platform:
CouchDB/PouchDB systems:
# 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;- Review each conflict record to understand what changed. Extract both versions for comparison:
# 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:
curl -u admin:password \ 'https://sync.example.org/fielddata/RECORD_ID?rev=CONFLICTING_REV_ID'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.
- Define field-level merge rules for records that support combining changes. In CouchDB, implement a conflict resolution function in your sync gateway configuration:
{ "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; } ` } } }- 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; } }- Set thresholds for automatic resolution. Not all conflicts should resolve automatically. Configure limits:
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: trueThis 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.
- Test automated rules with synthetic conflicts before deploying to production:
# 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 resultManual resolution procedures
When automated rules cannot resolve a conflict, or when the conflict involves protected data types, manual intervention is required.
- Retrieve complete details of both conflicting versions. Create a comparison document:
# 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.txtFor structured comparison, use a JSON diff tool:
jq -S '.' version_current.json > current_sorted.json jq -S '.' version_conflict.json > conflict_sorted.json diff current_sorted.json conflict_sorted.json- 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_byor 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
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
Apply the resolution by updating the document and removing conflict markers:
CouchDB: resolve by deleting losing revision
# 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
# 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.
- 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.
- 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';- 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" } }- 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.
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
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]Route escalation to appropriate authority:
Conflict Type Escalation Path Programme data Programme Manager Financial data Finance Manager Safeguarding/protection Safeguarding Lead HR/personnel data HR Manager Cross-programme Data Governance Lead Technical/systematic IT Manager 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:
# Verify no conflicts remain on the recordcurl -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 trackingcurl -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:
# Check required fields presentcurl -s -u admin:password \ 'https://sync.example.org/fielddata/RECORD_ID' \ | jq 'has("required_field1") and has("required_field2")'# Expected: true
# Verify data typescurl -s -u admin:password \ 'https://sync.example.org/fielddata/RECORD_ID' \ | jq '.numeric_field | type'# Expected: "number"Confirm sync propagation to downstream systems:
# Check replication statuscurl -s -u admin:password \ 'https://sync.example.org/_active_tasks' \ | jq '.[] | select(.type == "replication")'
# Verify record appears on targetcurl -s -u readonly:password \ 'https://reporting.example.org/fielddata/RECORD_ID' \ | jq '._rev'# Should match the resolved revisionVerify audit trail completeness:
SELECT conflict_id, record_id, resolution_type, resolved_by, resolved_at, resolution_notesFROM sync_conflictsWHERE record_id = 'RECORD_ID' AND resolution_status = 'resolved'ORDER BY resolved_at DESCLIMIT 1;The query should return the resolution record with all fields populated.
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| Conflict reappears after resolution | Offline device syncs with old conflicting revision | Identify device still holding conflict; force sync or clear local data on device |
| ”Conflict not found” when attempting resolution | Another administrator resolved the conflict; or conflict ID incorrect | Refresh conflict list; verify conflict ID; check resolution history |
| Resolution creates new conflict | Merge introduced data that conflicts with third version | Retrieve all revisions including the new conflict; resolve comprehensively |
| Cannot delete conflicting revision (403 Forbidden) | Insufficient permissions on database | Verify admin role; check database security rules for conflict resolution permissions |
| Automated resolution produces incorrect result | Rule logic error or edge case not covered | Disable automated resolution temporarily; resolve manually; update rule logic |
| User reports data loss after resolution | Wrong version selected as winner; or merge incomplete | Retrieve historical revisions from compaction-safe backup; restore lost data; re-resolve |
| Conflict involves deleted document | One version is deletion, other is update | Decide whether delete or update takes precedence based on business rules; apply resolution |
| Bulk conflicts after connectivity restoration | Many devices syncing simultaneously with overlapping changes | Prioritise by data criticality; consider temporary sync pause; resolve systematically |
| Resolution metadata not saved | Document update succeeded but metadata stripped | Check for document validation rules removing unknown fields; add _resolution to allowed fields |
| Downstream system shows different data than source | Replication lag or transformation issue | Force replication; check transformation rules; verify downstream system received update |
| Cannot identify users who made changes | modified_by field not populated | Check sync configuration captures user attribution; may need to resolve without user consultation |
| Conflict affects calculated/derived field | Both versions have stale calculations | Recalculate derived fields after resolving base data; do not merge calculated values |
Persistent conflict loops
When the same record repeatedly enters conflict state:
# Identify conflict frequency for a recordSELECT record_id, COUNT(*) as conflict_count, array_agg(DISTINCT local_user) as users_involvedFROM sync_conflictsWHERE record_id = 'PROBLEMATIC_RECORD_ID'GROUP BY record_id;If conflict count exceeds 3 within 30 days, investigate:
- Check if multiple users are assigned to the same data without coordination
- Verify sync frequency is adequate for the update rate
- Consider record locking during active editing sessions
- Review whether record should be split to reduce contention
Data loss recovery
If resolution accidentally discarded needed data:
# CouchDB: retrieve all historical revisionscurl -s -u admin:password \ 'https://sync.example.org/fielddata/RECORD_ID?revs_info=true' \ | jq '.["_revs_info"][] | select(.status == "available")'
# Retrieve specific historical revisioncurl -s -u admin:password \ 'https://sync.example.org/fielddata/RECORD_ID?rev=HISTORICAL_REV'If revisions have been compacted, restore from backup:
# Restore single document from backupcouchdb-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 modifyingasync 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 collectorsUPDATE data_collection_assignmentsSET 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
- Data Synchronisation Setup for configuring synchronisation systems
- Data Quality Remediation for addressing data quality issues discovered during conflict resolution
- Offline Data Architecture for architectural patterns that minimise conflicts
- Offline System Configuration for configuring offline operation
- Intermittent Connectivity Patterns for design patterns reducing conflict likelihood