Specify an Offline Edit Conflict Before Choosing a Merge

Drafted and reviewed using AI assistance.

On this page

An offline editor needs more than “sync when connected.” It needs a decision for the moment a local draft meets a newer saved record. Should the app keep the local text, show both versions, suggest a combination or refuse the write?

Describe the history before choosing a merge algorithm. Keep three things distinct: the version the editor started from, its unsaved draft, and the latest version read from the service. Then specify what a rejected write and an uncertain save should look like.

For an HTTP design, If-Match can make a write conditional on a matching entity tag. RFC 9110 requires strong comparison; when the condition fails, the requested method must not be performed. The RFC permits a successful response when an earlier application of the change can be established; otherwise the failed condition can produce 412 Precondition Failed. It does not define a field-merging policy. RFC 9110, section 13.1.1

Set up one fictional record

Two devices, Laptop A and Tablet B, edit a fictional note N17. Both have the permission needed for this exercise. The complete editable record has exactly two fields:

{"title":"Release checklist","body":"Check links."}

The service returns a strong entity tag with each version. Labels such as "n7" and "n8" below are invented opaque tags. Compare them for exact equality; do not parse their numbers or treat them as timestamps.

For these examples, choose a simple service contract: a replacement write supplies both fields and the tag it was based on. A matching write replaces the two fields together and receives a new tag. A mismatching write returns 412 and changes neither field. This fictional service does not use the RFC's already-applied success option. A missing tag is rejected by this application's policy too.

The comparison and write must form one conditional update in an implementation. Reading a tag, checking it in the client, and later issuing an unconditional write would not meet this contract.

On each device, preserve an editing bundle containing the record ID, base tag, complete base record and complete local draft. Refreshing the remote version must not overwrite that bundle. The histories below are hand-worked expectations, not traffic sent to a server.

Case one: the devices changed different fields

Both devices initially read "n7". Laptop A goes offline and revises the title. Tablet B stays online and revises the body.

Step Device event and write basis Latest saved tag Complete saved title Complete saved body
D0 A and B read the starting note "n7" Release checklist Check links.
D1 A edits offline; no write "n7" Release checklist Check links.
D2 B writes its body change with If-Match: "n7"; receives success "n8" Release checklist Check links and spelling.
D3 A reconnects and submits its full local draft with If-Match: "n7" "n8", unchanged; 412 Release checklist Check links and spelling.
D4 A explicitly reads the latest note; no write "n8" Release checklist Check links and spelling.
D5 A reviews the combined proposal and confirms a write based on "n8" "n9"; success Release checklist v2 Check links and spelling.

At D3, A's preserved editing bundle is still:

{
  "recordId": "N17",
  "baseTag": "\"n7\"",
  "base": {
    "title": "Release checklist",
    "body": "Check links."
  },
  "localDraft": {
    "title": "Release checklist v2",
    "body": "Check links."
  }
}

The service has not accepted that local draft. Showing “Saved” would be wrong. Replacing it with the latest remote record would also discard A's unsaved title.

After D4, compare each field against the original base:

Field Base at "n7" A's draft Remote at "n8" Chosen proposal
Title Release checklist Release checklist v2 Release checklist Take A's changed title
Body Check links. Check links. Check links and spelling. Take the remote changed body

This application offers a proposal because each field changed on only one side. That is an application rule, not an HTTP feature. A person still reviews the proposed combination before D5. For larger records, field independence may be a poor assumption: a title and body can depend on each other even when their keys differ.

The resulting illustrative request is:

PUT /notes/N17
If-Match: "n8"
Content-Type: application/json

{"title":"Release checklist v2","body":"Check links and spelling."}

These request shapes belong to the fictional service; they are not calls to a CueSpec API.

A newer edit can invalidate the reviewed proposal

Consider an alternative branch after D4, replacing D5. Tablet B changes the title to Urgent release checklist with If-Match: "n8"; the service saves that complete note as "n9", retaining the body Check links and spelling. A then submits the proposal above with "n8".

The service returns 412 again and preserves B's complete "n9" record. A keeps its draft and marks the proposal stale. It must read again and show the newly conflicting titles. It cannot merely replace "n8" with "n9" in the request and resend: that would turn a previously reviewed combination into permission to overwrite a later decision.

Until A makes a new choice based on the new record, another press of Save opens the conflict review and sends no write. This explicit pause is part of the chosen interface policy.

Case two: both devices changed the same field

Start an independent history from the same record values, this time tagged "s7". It is not a continuation of the "n9" history.

Step Event and write basis Saved tag Complete saved title Complete saved body
S0 Both devices read the starting note "s7" Release checklist Check links.
S1 A changes its offline title to Ready for review; no write "s7" Release checklist Check links.
S2 B saves title Needs wording review using "s7" "s8" Needs wording review Check links.
S3 A submits its local title and unchanged body using "s7" "s8", unchanged; 412 Needs wording review Check links.
S4 A reads "s8" and compares all three versions "s8", unchanged Needs wording review Check links.
S5 User explicitly enters Ready for wording review and saves with "s8" "s9"; success Ready for wording review Check links.

At S4, the title comparison contains three distinct values: the base is Release checklist, the local draft is Ready for review, and the remote value is Needs wording review. Neither change follows from the other. The application presents them together and preserves the local text while the user decides.

S5 uses a new title written by the user. The app has not inferred that combining two phrases preserves either writer's intent. Other deliberate choices are possible: keep the remote title, or choose the local title and submit it against the latest reviewed tag. The app should name that overwrite clearly.

If no choice is made, no new write occurs. Closing the conflict panel leaves the draft unresolved; it does not make the remote value an accepted resolution. Persisting that draft across a crash or application restart is a separate storage requirement that must be designed and tested.

A lost save response needs a different message

A timeout is not proof that the conditional update was rejected. Use another independent branch:

Step Event and write basis Actual saved tag in this fictional history Complete saved title Complete saved body What A knows
L0 Both devices read the starting note "l7" Release checklist Check links. Base is known
L1 A writes title Release checklist v2 with "l7"; service applies it, but the response is lost "l8" Release checklist v2 Check links. Save outcome is unknown
L2 B reads "l8", changes body, and saves with "l8" "l9" Release checklist v2 Check links and spelling. A has not read this version yet
L3 A explicitly refreshes and receives "l9" "l9", unchanged Release checklist v2 Check links and spelling. Current title matches its draft; current body has a later change

Before L3, show “Save outcome unknown. Check the latest version.” Keep the editing bundle. If the read also fails, that uncertainty remains.

After L3, A can say that the latest record contains its desired title. That observation does not identify which request or writer produced it. In this example there is no operation-receipt endpoint, so the original request's receipt remains unconfirmed even though the desired title is present. Let the user acknowledge the current record and clear the local pending draft deliberately; do not resend the old full record and erase B's newer body.

Choose the outcome before the algorithm

Situation Chosen next action Write allowed now? What remains preserved?
Old tag is rejected Read latest; show conflict review No automatic write Base and local draft
Different fields changed Offer combined proposal Only after review, with the latest reviewed tag Local draft until confirmed success
Same field has different values Show base, local and remote Only after explicit choice Unchosen local text until resolution
New remote version appears after review Invalidate proposal; read and decide again No stale proposal replay Draft and unresolved decision
Save response is lost Mark outcome unknown; read current record No automatic replacement write Pending editing bundle
Save pressed before resolution Return to conflict review No Unresolved draft

Copy an offline-conflict brief

Prepare a conflict-handling design for fictional note N17 using the
complete D, S and L histories in this article. Do not call an API or
implement production sync.

The editable record has exactly title and body. Treat quoted tags as
opaque strong validators. Our chosen service replaces both fields
only when the supplied tag matches; mismatches return 412 and apply
no change. It does not use an already-applied success optimization.

Keep record ID, base tag, complete base and local draft separately
from the latest remote read. A refresh must not erase local editing.
Offer a reviewed combination for the D case; require an explicit
same-field choice for S. If a newer version appears before a proposed
save, invalidate that proposal and obtain a new decision. Do not update
the tag and silently replay the old full replacement.

For L, distinguish unknown save receipt from a rejected write and from
observing the desired current title. Preserve the later body change.
Pressing Save before conflict resolution sends no write. State the
restart-persistence and atomic conditional-update requirements that
need implementation work rather than assuming they already exist.

Return the complete resulting records, UI states and decision steps.
Keep expected paper outcomes separate from any later executed checks.

You can choose automatic merging, a conflict panel or a simpler single-writer policy after the intended outcomes are clear. The useful starting point is a history that explains whose unsaved work remains, which version a write depends on, and what evidence permits the interface to say “Saved.”