Conflict messages should name the work
X8B6 replaced version numbers and overwrite buttons with explanations of the local action, confirmed state, and intervening change.
X8B6's first conflict dialog said:
This record has been modified by another user.
Reload or overwrite?The sentence was technically familiar and practically empty. It did not name the record, either change, or what overwrite would destroy. “Another user” was sometimes the same person on a second device. “Reload” sounded reversible even though it discarded pending local intent.
The synchronization engine had enough information to explain the work. The interface was throwing that information away at the moment it mattered most.
The message began with the local action
Someone encountering a conflict first needed to recognize what they had tried to do.
The dialog changed from a generic warning to a sentence:
You counted 10 lamps on shelf A while this device was offline.For an adjustment it might say “You recorded 2 lamps removed from shelf A.” For a move: “You moved 3 chairs from storage to the front room.” The operation kind supplied the verb.
This wording did not assume the operation had reached shared state. It named the pending intent stored on the device. The time and device appeared in detail when useful.
I avoided “your version.” A version was an implementation artifact. The person had performed or recorded an action. Naming it made the following choice concrete.
The same formatter powered the outbox and history, so conflict UI did not invent a new description of the operation.
Shared state needed a story too
The next line explained what X8B6 had learned since the local base:
Since the quantity shown on this device, 1 lamp was removed and confirmed.
Shared quantity is now 11.When several intervening operations mattered, the interface summarized them and offered a timeline. It did not dump raw events into the decision surface.
The phrase “confirmed” distinguished server receipts from another device's merely pending work. Authorship appeared only when it helped clarification and policy allowed it. The operation and location were usually more relevant than turning the conflict into a contest between people.
For an archived item, the story changed: “This item was archived after your last confirmed view.” For a renamed location, it said that the destination now had a different name and preserved stable identity.
The goal was causal context. The server revision had changed because specific accepted work occurred. The interface showed the work.
Early drafts said “Your version: 83; server version: 84.” The numbers proved there was an ordering difference and offered no basis for resolving it.
Revisions remained useful in diagnostics and support copy. A detail section could show local base 83, current 84, operation ID, and receipt IDs. That information helped reproduce a bug without pretending it was product language.
The main explanation translated revisions into their consequence. If revision 84 was an unrelated note, the local adjustment might not conflict at all. If it archived the item, the same one-number difference was decisive.
This prevented technical precision from becoming communicative precision. A number can be exact and still answer the wrong question.
I used a copyable diagnostic block instead of placing identifiers in every sentence. People making the decision saw tasks and quantities; people investigating the protocol could still reach the evidence.
Buttons described the resulting state
“Reload” and “Overwrite” were implementation verbs. I replaced them with operation-specific consequences.
For the recount example:
Use 10 as the new confirmed count
Apply the later removal after the count
Keep shared quantity 11
Review laterThose options appeared only when the evidence supported them. If chronology between physical count and removal was uncertain, the second choice explained its assumption. If an operation could compose safely, the system did so and reported the outcome instead of asking a needless question.
Buttons remained concise, with supporting text nearby. The destructive consequence was not hidden in a confirmation after a vague first click.
“Review later” preserved the operation and paused its dependency chain. Closing the dialog did not mean discard.
The action label answered, “What will X8B6 believe after I choose this?”
The interface did not call every case a conflict
The server returned several non-success outcomes. They needed different language.
An invalid negative adjustment was rejected: “Removing 5 would put shelf A below zero.” Authentication expiry said the action remained stored and required signing in. A network timeout said no outcome had been received. A missing item explained that the item had been archived or deleted.
Only competing legitimate work that needed policy appeared as review.
This distinction prevented the generic dialog from offering overwrite after a permission failure or retry after a semantic rejection. It also made metrics more meaningful: review volume described actual concurrency decisions, not every failed request.
The UI vocabulary mirrored the operation states:
waiting to send
checking outcome
needs review
rejected
waiting for sign-in
confirmedSpecific state names produced specific next actions.
“Changed elsewhere” had no place
“Elsewhere” hid both place and relationship. A change could come from another device, a server-side administrative action, an import, or a retry whose original receipt had just been recovered.
X8B6 named the source when it was known and relevant:
Confirmed on another device
Applied by the inventory import
Existing receipt recovered for this device's operationIt did not speculate. Unknown origin remained “A confirmed change” with its operation time and effect.
The same person could use two devices, so “another user” was often false. Device identity helped diagnostics but rarely belonged in the headline. “Another device” described the boundary without inventing an actor.
Removing “elsewhere” forced the product to state what evidence it actually had about origin.
Dates explained observation and confirmation
Offline work had more than one relevant time. A recount could be observed at 14:20, recorded locally at 14:22, and confirmed after reconnection at 15:05. An intervening removal might have been observed at 14:25 and confirmed at 14:26.
The review interface prioritized observation time when comparing physical events and confirmation order when explaining shared state. It labeled both rather than sorting everything by one updatedAt.
Your count: observed 14:20, waiting on this device
Removal: observed 14:25, confirmed 14:26If a device clock was uncertain, the wording reduced precision and said so. The system did not infer physical order from unreliable timestamps.
This helped a person decide whether the removal occurred after the count. A version number alone could show server order and not real-world observation order.
Time labels served the decision, not the database schema.
A preview made consequences inspectable
For quantity decisions, X8B6 showed a small before-and-after preview:
Confirmed now: 11
If the count becomes authoritative: 10
If the later removal follows the count: 9The preview came from the same deterministic transition functions the server used. It did not calculate separately in the view with slightly different rules.
Pending dependent operations were included or listed explicitly. Choosing a recount could change the projected base for a later move. The screen named those downstream effects before confirmation.
I avoided coloring one outcome green as the recommended “safe” answer unless product policy genuinely preferred it. The system lacked enough evidence to resolve the case automatically; visual styling should not smuggle in a choice.
Preview turned an abstract merge into quantities and tasks. It also exposed bugs: if the preview could not explain the resulting projection, the resolution operation was underspecified.
The first conflict interrupted sync with a modal. Dismissing it left no obvious way back. A tablet with several offline conflicts became a sequence of blocking dialogs.
I moved review-required operations into the ledger. A banner summarized affected items, and the person could finish unrelated work before reviewing. Each decision surface had a stable URL-like view within the app and restored after reload.
The pending operation remained in IndexedDB with its review context. The server result named the base and intervening operations. Reopening did not reconstruct the message from whatever the current record happened to be.
If shared state advanced, the review refreshed and invalidated options based on the older revision. It preserved the local intent while asking for a decision against the newest evidence.
This made conflict resolution a workflow with lifecycle, not a transient error dialog. The interface could be interrupted without losing the question.
Dependency language named blocked work
A conflicted move could block a later adjustment that assumed the item reached its destination. The outbox originally labeled the later operation “pending,” which made it look ready to send.
The ledger said:
Move 2 lamps to shelf B — needs review
Remove 1 lamp from shelf B — waiting for the move aboveOpening the dependent row linked back to the unresolved premise. It did not offer retry.
For longer chains, the summary showed how many operations waited and which item they affected. Unrelated operations continued synchronizing, so the page did not announce a global failure.
The wording treated dependency as work structure rather than a queue error. “Blocked” alone would identify a condition without naming what needed to happen.
This was important during handoff. A person could see that resolving one move would release two later actions, while the rest of the device was already confirmed.
If the local operation itself was wrong, the choice was not simply discard or overwrite. A confirmed operation required a correction; an unconfirmed one could be discarded with an explicit record in the local ledger.
The interface used different verbs:
- “Discard this unconfirmed recount” when no server effect existed.
- “Correct the confirmed adjustment” when a receipt existed.
- “Replace this draft” before an operation identity had been committed.
That distinction kept the UI from implying that confirmed shared history could vanish. A correction preview showed the compensating effect and linked the new operation to the original.
After correction, history said “Corrected by…” rather than editing the old row in place. The current projection became accurate without manufacturing a past in which the mistake never occurred.
Clear language followed durable state. The product could say discard only where discard was actually possible.
I showed the review screens without explaining the protocol and asked people to answer:
- What did this device record?
- What changed in shared inventory?
- What will each button do to the quantity?
- Which work will remain if you decide later?
- Has either action already been confirmed?
The generic dialog failed immediately. People understood that a collision existed and guessed at the buttons.
The first detailed version failed differently: it contained every timestamp and revision, so readers spent attention reconstructing the chronology. Grouping the screen into local action, confirmed changes, resulting choices, and technical detail improved answers.
One phrase—“accept server version”—was consistently interpreted as a temporary refresh. It actually discarded the local operation. “Keep shared quantity 11 and discard this recount” was longer and accurate.
The sessions evaluated the belief the interface produced, not preference for shorter copy.
Conflict states initially used a yellow card and a triangle icon. The revised screen had a real heading, operation-specific summary, ordered chronology, and labeled controls. Color reinforced review status without carrying it alone.
Focus moved to the heading when the review view opened and returned to the ledger row after a decision. Validation errors appeared beside the affected choice and in a summary. Dynamic refresh announced that shared state had changed without discarding the local operation.
The before-and-after quantities used text labels rather than relying on columns whose relationship might disappear under zoom. Button descriptions remained distinguishable when read outside their visual grouping.
Keyboard navigation covered “review later” as an ordinary action rather than forcing dismissal with Escape, whose meaning was unclear.
These details mattered because the decision already required concentration. The interface should not add uncertainty about structure or control ownership.
The server returned explainable context
The first API returned 409 Conflict and a current record. The browser could not explain which accepted actions intervened or which merge policy applied.
The review result became structured:
interface ReviewResult {
operationId: string;
reason: string;
localBaseRevision: number;
currentRevision: number;
relevantOperations: OperationSummary[];
allowedResolutions: ResolutionKind[];
}The server selected relevant operations according to domain policy, not merely every event since the base. The browser translated known reason and resolution codes into product language. Unknown codes produced a safe unsupported state and preserved diagnostics.
I avoided shipping ready-made English from deep server code as the only representation. Structured context supported different layouts and future wording while keeping allowed actions authoritative.
The API improvement followed the UI need. Explainable conflict handling required the protocol to retain more than a status code and latest row.
Free-form assembly produced grammatical errors and missing subjects. I defined formatters per operation and review reason.
An adjustment formatter knew quantity direction, amount, item label, and location. A recount formatter knew observed quantity and time. An archive conflict formatter knew the item was no longer active. Shared helpers handled dates and attribution.
Templates received validated view models, not raw optional payloads. If required context was missing, the system chose a conservative fallback: “This recount needs review; its item context could not be loaded.” It did not produce “Moved undefined to undefined.”
Sentence-case headings and ordinary prose kept the review consistent with the rest of the product. Technical codes stayed in detail.
I added snapshot tests for representative language and unit tests for quantities such as one item, plural items, zero, and negative direction. Wording was product behavior; regressions could change the apparent consequence of a choice.
The best message sometimes avoided review
Improving conflict copy revealed cases where the person did not need a message at all. Two independent notes could coexist. Two safe adjustments could compose. A duplicate operation could return its existing receipt.
The system applied those policies automatically and recorded a quiet explanation in history. Interrupting people to confirm a consequence already determined by the domain would make the product feel less reliable.
Review remained for genuine uncertainty or consequential choice. Its volume became a signal about the operation model. If one case occurred frequently, the project could improve its verb, capture better base evidence, or define a safe policy rather than polishing the same dialog forever.
Good conflict UX includes reducing avoidable conflicts before writing them.
A resolution did not disappear after its button was pressed. The resulting receipt linked the original pending operation, the intervening confirmed work, the choice made, and the new confirmed revision.
History could then say “Recount set quantity to 10 after reviewing removal at revision 84” or “Local recount discarded; shared quantity 11 kept.” The language was concise, with full identifiers in detail.
This record helped during later confusion. A quantity did not merely jump after a conflict dialog; the system could explain which evidence a person chose and what work remained. Reopening the item did not require remembering a transient modal.
Decision history also held the interface accountable. If a button label promised to apply the later removal after the count, the stored result had to demonstrate that exact projection. Review copy and protocol outcome could be tested as one path.
Naming work changed the architecture
Replacing “record modified” with a useful sentence required more than copy. X8B6 needed explicit operation kinds, stable identities, base revisions, receipts, intervening-operation summaries, and resolution actions with their own outcomes.
The interface exposed what the record-replacement model had omitted. Once the system could name “counted ten” and “removed one,” it could also preserve both, reason about chronology, preview results, and pause dependent work.
The final review screen was longer than the original dialog and faster to understand. It spent words where the system lacked certainty and kept routine composition quiet.
I still distrust conflict messages built around “your version” and “server version.” They ask people to choose between storage artifacts. A responsible interface names the work on each side, the evidence connecting it, and the consequence of every available action.
If the system cannot form those sentences, the problem probably began before the copy.