| name | conflict-resolution-backend |
| description | Resolve concurrent writes from multiple mobile devices -- last-write-wins, 3-way merge, and vector clocks. Use when designing server-side merge semantics for offline-capable apps. |
Conflict Resolution (Backend)
Instructions
Two devices edit the same record offline, then sync. The server must decide which edit wins, or how to combine them. Pick the simplest strategy that meets the domain requirement; complex schemes are expensive and buggy.
1. Conflict Detection
A mutation carries base_rev -- the revision the client saw before editing (see sync-protocols). Conflict exists when the server's current rev > base_rev for that entity.
client_A edits at base_rev=10 -> server rev is now 12 -> CONFLICT
Never auto-resolve silently for domains where silent loss is unacceptable (money, medical, legal).
2. Last-Write-Wins (LWW)
Default for domains where staleness is tolerable (profile bio, preferences, cache-like data).
Mechanism: the incoming mutation overwrites if it is newer by some tie-breaker (server timestamp on arrival, or an HLC). Include a monotonic updated_at so clients can display "last edited on device X".
UPDATE articles
SET data = $new_data, rev = nextval('article_rev'), updated_at = now()
WHERE id = $id AND ($base_rev IS NULL OR rev <= $base_rev + GRACE);
Pros: simple. Cons: loses the loser's work without telling anyone.
Mitigations: keep a short audit trail of overwritten versions so a user can recover.
3. Reject-on-Conflict (Optimistic Concurrency)
For domains where losing a write silently is unacceptable (documents, forms, inventory counts).
Server replies 409 CONFLICT with the current entity. The client presents a merge UI or pulls + re-applies its change.
{ "status": "conflict", "current_rev": 4815, "current_entity": { "...": "..." } }
Clients must handle this explicitly -- not a retry loop.
4. Three-Way Merge
Works when entities have independently mergeable fields.
Server stores: base (the common ancestor), theirs (current server state), ours (incoming). For each field:
- If only
ours differs from base -> take ours.
- If only
theirs differs from base -> take theirs.
- If both differ -> conflict on that field.
Best for structured records with many fields (contact card, settings object).
function merge<T extends object>(base: T, ours: T, theirs: T): { merged: T; conflicts: string[] } {
const merged: any = { ...theirs };
const conflicts: string[] = [];
for (const k of Object.keys(ours) as (keyof T)[]) {
const b = base?.[k], o = ours[k], t = theirs[k];
if (deepEqual(o, b)) continue;
if (deepEqual(t, b)) { merged[k] = o; continue; }
if (deepEqual(o, t)) continue;
conflicts.push(k as string);
}
return { merged, conflicts };
}
Return conflicts to the client; if empty, commit. Otherwise 409 with the conflicting field names.
5. Vector Clocks and HLC
Useful in multi-region or peer-to-peer systems where no single authoritative clock exists. Each node increments its own counter; the vector {nodeA: 7, nodeB: 3} lets you detect concurrent edits vs causal descendants.
In practice, most mobile backends have one authoritative region at a time (failover is rare). A single monotonic rev plus updated_at is usually enough. Reach for vector clocks only if you need eventual consistency across active regions.
Hybrid Logical Clocks (HLC) sit between wall-clock and logical clocks and tolerate small clock skew; a good choice when you want "mostly wall-clock" with logical tie-breakers.
6. Field-Level Last-Write-Wins
For records with many independent fields (settings), LWW per field reduces false conflicts:
- Each field stores
{value, updated_at, updated_by}.
- Merges take the field with the highest
updated_at.
This is essentially an LWW-map CRDT; see crdt-basics.
7. Append-Only Streams
For activity logs, message histories, and events, avoid in-place edits. Writes are appended; conflicts do not exist (each device's events interleave by server timestamp on arrival).
8. Audit Trail
For any non-trivial strategy, keep a version history table:
CREATE TABLE article_versions (
id TEXT,
rev BIGINT,
author TEXT,
data JSONB,
PRIMARY KEY (id, rev)
);
Enables undo, forensic debugging, and "history" UIs. Prune on a retention policy.
9. Client UX
- If the resolution is automatic and lossless (CRDT, additive), apply silently.
- If lossy (LWW), consider a toast: "Your change was merged with an edit from iPad."
- If reject-on-conflict, show a merge screen with
ours vs theirs.
10. Testing
Conflict code is high-risk; test with explicit scenarios:
- Same field, same value edited twice -> no conflict.
- Disjoint fields edited -> merged cleanly.
- Same field, different values -> conflict reported.
- Delete vs edit -> domain decision (usually delete wins, edit discarded, with audit trail).
Checklist