Skip to content
Preptima
hardDesignConceptMidSeniorStaff

Two users edit the same record while offline and both reconnect. How do you reconcile the two versions without silently losing one of the edits?

Accepting writes on both sides means conflicts are inevitable, so the design choice is who resolves them. Last-write-wins discards an edit by clock accident; version vectors detect the conflict and hand it back; CRDTs merge deterministically but only for the operations they model.

6 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Whether the candidate distinguishes detecting a conflict from resolving one, and says which the system does
  • Does the answer notice that last-write-wins depends on clocks that are not synchronised
  • That the resolution granularity is chosen deliberately, per document versus per field versus per operation
  • Whether deletion is recognised as the case that breaks naive merging
  • Can they name an operation their chosen merge cannot handle, rather than presenting one as universal

Answer

The conflict is a consequence of a decision already made

If you let both clients accept writes while disconnected, you have chosen availability over a single authoritative order, and divergent versions are the arithmetic of that choice rather than a bug in it. There is no reconciliation strategy that recovers a total order you declined to impose. So the honest opening is that you cannot avoid the conflict, only decide who resolves it and with how much information.

That reframing matters because it exposes the alternative nobody states. If losing an edit is unacceptable and automatic merging is unacceptable, the answer is to stop taking offline writes for that data — make the field read-only when disconnected, or take a lock that must be acquired online. Refusing the write is a legitimate design, and a candidate who names it and then explains why it is unaffordable here is stronger than one who assumes offline editing is a requirement because it was mentioned.

Last-write-wins loses whichever edit had the slower clock

The default in most systems, because it is one comparison and one column, is to keep the version with the later timestamp. Its cost is exact and worth stating precisely: it does not resolve the conflict, it discards one side of it, and it chooses which side using a clock you do not control.

Client clocks drift, are set by the user, and can be wrong by minutes. A device whose clock runs fast wins every conflict it enters, indefinitely, and the user on the correct clock watches their work vanish with no error and no trace. Moving to server-assigned timestamps removes the malicious case but not the fundamental one, because the server timestamp records arrival order, and arrival order for offline clients is decided by which one found a network first. The edit made three hours ago on a laptop that reconnected last overwrites the edit made ten minutes ago on a phone.

Last-write-wins is nonetheless the right answer for a genuine class of data: values where only the latest observation has meaning. A presence indicator, a cursor position, a device's own battery reading. The test is whether the discarded value was information or merely a stale sample. Where it was information, using last-write-wins is data loss you have chosen not to log.

Version vectors turn silent loss into a detected conflict

The machinery that lets you tell a genuine conflict apart from a legitimate overwrite is causal, not temporal. Each replica keeps a counter, a version is tagged with the counters it has seen, and comparing two versions gives one of three answers rather than two: one descends from the other, so it is simply newer and can be applied; or neither descends from the other, in which case they are concurrent and you have a real conflict.

flowchart TD
  A[Two versions arrive] --> B{Compare version vectors}
  B -- A dominates B --> C[Keep A, B is stale]
  B -- B dominates A --> D[Keep B, A is stale]
  B -- neither dominates --> E[Concurrent edit]
  E --> F{Can the type merge itself}
  F -- yes --> G[Merge deterministically]
  F -- no --> H[Keep both and ask the user]

The branch that carries the weight is the last one, because a system that has detected a conflict has not yet solved anything — it has only earned the right to choose deliberately instead of by clock accident.

The costs are real. The vector grows with the number of replicas that have written, which is fine for a handful of devices and unpleasant for a large fleet of clients, so implementations prune entries and accept a small chance of a false conflict. And a conflict handed to the user is a user-experience problem you have to design: a version-history panel, an inline "this changed elsewhere" prompt, or in the worst case two records where there should be one. Deferring to the user is correct when the semantics are genuinely ambiguous and unacceptable when it happens forty times a day, so the practical design detects conflicts everywhere and asks the user only for the fields where automatic merging would be wrong.

What CRDTs actually buy, and what they do not

A conflict-free replicated data type is a type whose merge function is commutative, associative and idempotent, so replicas that have seen the same set of updates in any order converge to the same value without coordination. Text editing is the well-known case, and a sequence CRDT is why two people can type into the same paragraph offline and get a sensible combined result rather than one paragraph replacing the other.

The important limitation is that the guarantee belongs to the type, not to your application. A CRDT promises convergence, meaning every replica ends up agreeing. It does not promise that the value they agree on satisfies your business rules. A counter that both replicas decrement can converge on a negative stock level; a set that one replica adds to while another removes from converges according to whichever bias the type was built with, and the outcome is deterministic without being the one anybody wanted. Invariants that span fields — a start date that must precede an end date, when two clients edited one field each — survive no merge that operates field by field.

So the design pattern that works is a split. Model the collaborative, additive, high-conflict parts of the document as CRDTs and let them merge without a round trip. Keep the parts that carry invariants on the server, behind a conditional write that fails if the state moved, and let those writes be the ones that require connectivity. Answering that a CRDT solves the problem, without naming a value it would converge on incorrectly, is the response that stops at the marketing.

Deletion is where the naive merge breaks

The case to walk through unprompted, because it is where most hand-rolled reconciliation fails, is a delete concurrent with an edit. One user deletes the record while offline. The other edits it. Both reconnect.

If deletion removes the row, the deleting replica now has no record and therefore no version to compare against, so the incoming edit looks like a new object rather than a conflicting one. The edit resurrects the deleted record on every replica, and the user who deleted it watches it come back with no explanation. The same shape breaks item removal inside a collection: remove and re-add race, the removal has nothing to attach to, and the item persists.

The fix is that deletion is a write like any other. It sets a tombstone carrying its own version information, so a later edit can be compared against it and the conflict can be seen. That is why offline-capable systems accumulate tombstones, and why the follow-on problem is when you may collect them: delete a tombstone before every replica has seen it, and a replica that has been offline since before the deletion will re-introduce the record on its next synchronisation. Collection is therefore bounded by how long you allow a client to be absent, which converts a storage question into a product policy — after that window, the client resynchronises from scratch rather than merging.

Conflicts are the price of accepting writes on both sides, so the only real decision is who pays it: last-write-wins pays with an edit nobody is told about, version vectors pay with the complexity of detection plus a user prompt, and CRDTs pay by restricting you to operations whose merge is defined — and none of them will preserve an invariant you have not kept on the server.

Likely follow-ups

  • Your merge is per field and two users edited different fields of the same record. Why might that still produce an invalid record?
  • How do you keep tombstones from growing without bound, and what breaks when you finally collect them?
  • Where would you put a server-side invariant, such as a balance that cannot go negative, in a design that merges client writes?
  • The two offline clients are the same user on two devices. Does that make automatic resolution more or less acceptable?

Related questions

conflict-resolutioncrdtversion-vectorsoffline-firsteventual-consistency