# Conflict Resolution

> Pragmatic conflict resolution for mobile sync - last-write-wins, field-level merge, three-way merge, vector clocks, and CRDT basics. Use when two clients can modify the same record offline.

- Skill: `almasumdev/conflict-resolution` (Agent Skill)
- Install (CLI): `npx skillmds@latest add almasumdev/conflict-resolution`
- Raw SKILL.md: https://api.skillmd.com/api/skills/almasumdev/conflict-resolution/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: almasumdev (https://skillmd.com/u/almasumdev)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/almasumdev/conflict-resolution

---


# Conflict Resolution

## Instructions

Any bidirectional sync has conflicts. The question is not "will we have them" but "how do we resolve them predictably". Pick a policy per entity, make it explicit, and show pending or conflicted states in the UI.

### 1. Conflict Models

| Model | Works by | Good for |
| --- | --- | --- |
| Last-write-wins (LWW) | Highest timestamp wins per record | Settings, profile fields, simple data |
| Field-level LWW | Per-field timestamps merge | User profile with independently edited fields |
| Three-way merge | Base + ours + theirs -> merged | Documents, code-like content |
| Vector clocks | Track causality | Systems with unreliable clocks |
| CRDTs | Conflict-free data types | Collaborative docs, shared sets |
| Manual | Ask the user | Irreducible conflicts, high-stakes data |

### 2. Last-Write-Wins

The easiest policy. Each update carries a server-authoritative timestamp or revision.

```kotlin
data class Article(val id: String, val title: String, val body: String, val rev: Long, val updatedAt: Instant)

fun resolve(local: Article, remote: Article): Article =
    if (remote.rev >= local.rev) remote else local
```

Trap: naive wall-clock LWW drops edits when devices have skewed clocks. Use server-assigned revisions or hybrid logical clocks (HLC).

### 3. Field-Level LWW

Good when different fields are edited on different devices.

```
record: { title: {v:"A", ts:t1}, body: {v:"B", ts:t2} }
```

On merge, pick the higher timestamp per field. Keep the change envelope small by sending only changed fields.

### 4. Three-Way Merge

When you know the base version the client started from, the server can merge:

```
merge(base, ours, theirs) =
  if ours == base: theirs
  elif theirs == base: ours
  elif ours == theirs: ours
  else: conflict -> policy (prefer ours, prefer theirs, prompt user)
```

For text, use a diff/patch library. For structured records, do it field-by-field.

### 5. Vector Clocks

When wall clocks cannot be trusted and causality matters:

- Each replica keeps a vector `{replicaId: counter}`.
- On update, increment the local replica's counter.
- On merge, compare vectors:
    - If A <= B (every component), B is newer.
    - If neither dominates, concurrent -> conflict.

Vector clocks detect conflicts; they do not resolve them. Pair with a resolution policy.

### 6. CRDTs (Conflict-Free Replicated Data Types)

CRDTs guarantee automatic merge without conflicts:

- **Counters** (G-Counter, PN-Counter) -- increments merge by summing per replica.
- **Sets** (OR-Set, LWW-Set) -- add/remove operations merge deterministically.
- **Text** (RGA, Yjs, Automerge) -- concurrent edits merge into a consistent sequence.
- **Maps** -- nested CRDTs.

Use Yjs, Automerge, or a language-native port. Cost: payload growth from metadata, operation history, and garbage collection.

### 7. Pragmatic Recipe for Most Apps

1. Default to **server-assigned revisions** + **LWW** at the record level.
2. Switch to **field-level LWW** for profile-like entities with independent edits.
3. Switch to **three-way merge** for documents with structured content.
4. Reserve **CRDTs** for truly collaborative content (multi-cursor docs, shared boards).
5. Always have a **manual fallback** for high-stakes data (medical, financial).

### 8. Client UI for Conflicts

Do not fail silently. Conflicts need affordances:

- **Pending** state: local change sent, awaiting ack.
- **Synced** state: server ack received.
- **Conflict** state: show both versions, let the user pick, or default to policy with undo.
- Keep an in-app history/log of conflict resolutions for 7-30 days.

```swift
enum SyncState: Equatable {
    case synced
    case pending
    case conflict(local: Article, remote: Article)
    case failed(reason: String)
}
```

### 9. Deterministic Behavior

- Every merge function is pure and testable. Write unit tests that exercise LWW, concurrent edits, and deletions.
- Deletes need special handling: use **tombstones** so a delete does not get resurrected by a stale offline edit.
- Document idempotency: applying the same resolution twice must yield the same state.

### 10. Anti-Patterns

- Silent server overwrites. The user loses work without signal.
- Client-side timestamps as the source of truth without clock validation.
- Mixing CRDTs and LWW within one entity.
- Resolving conflicts in UI code. Keep the policy in a domain function; the UI presents outcomes.
- Letting tombstones accumulate forever. Garbage collect.

### 11. Testing Conflicts

- Simulate two clients editing the same record offline, then reconnecting in either order.
- Property-based tests for merge commutativity (`merge(a,b) == merge(b,a)`) where applicable.
- Fuzz the resolution function with random histories.
- In CI, run an end-to-end scenario with two emulated devices against a test server.

## Checklist

- [ ] A conflict policy is chosen and documented per entity.
- [ ] Server-assigned revisions or HLCs are used, not wall clocks alone.
- [ ] Delete operations use tombstones with a GC policy.
- [ ] Merge functions are pure, unit-tested, and idempotent.
- [ ] UI surfaces pending, synced, and conflict states.
- [ ] CRDTs are used only where justified; not as a default.
- [ ] Conflict rate is monitored per entity; spikes trigger review.
- [ ] End-to-end scenarios simulate concurrent offline edits.

