Skip to content

Conflict Resolution

The Tech Strategy Tool uses optimistic concurrency with sequence-number-based conflict detection. This page explains how concurrent edits are detected, how conflicts are surfaced to users, and how the system resolves them.

The Problem

In a collaborative editing environment, two users may edit the same field simultaneously. Without conflict detection, the last write silently overwrites the first, causing data loss that neither user is aware of.

sequenceDiagram
    participant Alice
    participant Server
    participant Bob

    Alice->>Server: Read principle title (seq 42)
    Bob->>Server: Read principle title (seq 42)
    Alice->>Server: Update title to "API-First" (lastSeenSequence: 42)
    Server-->>Alice: Applied (seq 43)
    Bob->>Server: Update title to "Cloud-Native" (lastSeenSequence: 42)
    Server-->>Bob: Rejected (conflictingServerValue: "API-First")

How It Works

Sequence tracking

Every field in the strategy document has an associated sequence number representing the last event that modified it. When a user starts editing a field, the client captures the current sequence number as lastSeenSequence.

Conflict detection

When an edit event includes lastSeenSequence, the processor uses the HasConflict helper to compare:

  1. Find the event that last modified this field
  2. Compare its sequence number to the lastSeenSequence in the request
  3. If lastSeenSequence < lastModifiedSequence, the field has been changed since the user last read it — conflict detected

Resolution

On conflict, the processor returns:

Field Value
status "rejected"
rejectionReason "Conflict: field was modified since your last read"
conflictingServerValue The current server value the client is out of sync with

PreviousValue vs ConflictingServerValue

The processor's DispatchResult has two separate fields: PreviousValue (only set on applied events — the value before the change, for history) and ConflictingServerValue (only set on conflict rejections — the current server value the client is stale against). These fields are mutually exclusive. The API's SubmitEventResponse mirrors this split with previousValue and conflictingServerValue fields.

Supported Fields

Conflict detection is available on the 7 field-edit events that use lastSeenSequence:

Event Type Conflicted Field
update_group_name Group name
update_group_description Group description
update_principle_title Principle title
update_principle_description Principle description
update_objective_title Objective title
update_initiative_name Initiative name
update_initiative_progress Initiative progress

Other update events (team name/color, Jira key, group/principle/objective/initiative assignment and reorder) do not use conflict detection.

Client-Side Handling

EditableField state machine

The EditableField component in the UI manages the user experience around conflicts:

stateDiagram-v2
    [*] --> Display
    Display --> Edit: Click
    Edit --> Persisting: Blur/Enter
    Edit --> Display: Escape
    Persisting --> Display: Success
    Persisting --> Error: Conflict/Failure
    Error --> Persisting: Retry
    Error --> Display: Cancel
  1. Display — the field shows its current value as static text
  2. Edit — the user clicks to enter edit mode; lastSeenSequence is captured
  3. Persisting — the edit is submitted to the server; a spinner is shown
  4. Error — if the server rejects with a conflict, the error state shows:
    • The current server value (from conflictingServerValue in the response)
    • A Retry button to re-submit the edit
    • A Cancel button to accept the server's value and return to display

Timeout fallback

The EditableField component has a ~30-second timeout on the persisting state. If no response is received, it fetches the card from the API to determine the current state and exits the persisting state.

Design Rationale

Why optimistic concurrency?

The Tech Strategy Tool uses optimistic concurrency (detect conflicts after the fact) rather than pessimistic locking (prevent concurrent access):

  • No lock management — no need to handle stale locks, lock expiry, or deadlocks
  • Non-blocking reads — users can always see the latest data without waiting for locks
  • Natural for event sourcing — sequence numbers provide a natural ordering for conflict detection
  • Low conflict rate — in practice, strategy editing is low-frequency enough that conflicts are rare

Why per-field tracking?

Conflicts are tracked at the field level, not the entity level. This means:

  • Editing a principle's title while another user edits its description does not create a conflict
  • Only truly conflicting edits (same field, same entity) are rejected
  • This minimizes false conflicts in collaborative editing

Why reject rather than merge?

The system rejects conflicting edits rather than attempting automatic merging:

  • Text merging is complex and error-prone for short fields like titles
  • Explicit rejection ensures the user sees the conflict and makes a conscious decision
  • The current server value is provided so the user can make an informed choice

Example: Conflict Flow

Here is a complete example of a conflict being detected and resolved:

  1. Alice opens principle "API Design" (last modified at seq 42)
  2. Bob opens the same principle (also sees seq 42)
  3. Alice edits the title to "API-First Design" and saves
    • Server applies the change at seq 43
    • SSE broadcasts card-changed to all clients
  4. Bob edits the title to "Cloud-Native Design" and saves
    • Request includes lastSeenSequence: 42
    • Server detects that seq 42 < seq 43 (the field was modified)
    • Server rejects with conflictingServerValue: "API-First Design"
  5. Bob sees an error with the current server value "API-First Design"
  6. Bob can either:
    • Retry — re-submit "Cloud-Native Design" (this time with the updated sequence)
    • Cancel — accept Alice's "API-First Design" and return to display mode