Optimistic locking

Preventing lost updates with version-based concurrency control

circle-exclamation

Navixy Repository API uses a version field for optimistic concurrency control, preventing lost updates when multiple clients simultaneously edit the same entity.

How optimistic locking works

Every versioned entity includes a version field:

type Device {
  id: ID!
  version: Int!
  name: String!
  # ...
}

The version starts at 1 when an entity is created and increments with each successful update.

Update and delete mutations require the current version:

input UpdateDeviceInput {
  id: UUID!
  version: Int!   # Required
  title: String
  # ...
}

input DeleteDeviceInput {
  id: UUID!
  version: Int!   # Required
}

Supported entities

Optimistic locking applies to:

Entity
Description

GPS trackers, sensors, beacons

Vehicles, equipment, employees

Asset collections

Geofences, POIs, routes

Work hours, maintenance windows

Warehouse records

Organization hierarchy nodes

User accounts

Organization memberships

External system integrations

Custom field metadata

All catalog items (device types, asset types, roles, tags, etc.)

Operations by type

Operation
Version in input
Behavior

Create

Not required

Returns version: 1

Update

Required

Returns CONFLICT error on mismatch

Delete

Required

Returns CONFLICT error on mismatch

Workflow

Here's the typical flow for updating an entity:

  1. Query returns version:

  1. Mutation requires version in input:

Handling conflicts

If the entity was modified since you fetched it, the API returns a CONFLICT error. The HTTP status code will be 200 because the request was successfully received and processed — the "conflict" is a business logic outcome, not a transport failure. This follows the GraphQL-over-HTTP specificationarrow-up-right, which reserves HTTP error codes (4xx, 5xx) for transport-level problems like authentication failures or malformed requests.

The actual error details are in the response body:

Use extensions.code for programmatic error handling, not HTTP status codes. See Error handling for more details.

When you receive a conflict error:

  1. Fetch the entity again to see what changed

  2. Merge the other user's changes with yours if needed

  3. Retry your update with the new version

Concurrent editing example

Here's what happens when two users edit the same device:

User A's update succeeds first. User B's update fails because the version changed. After refetching, User B can successfully update with the current version.

Idempotent commands

Mutations that manage relationships and assignments are called idempotent commands. They do not require or check the version field.

Mutation
Purpose

Link device to inventory

Unlink device from inventory

Add identifier to device

Remove identifier from device

Add asset to group

Remove asset from group

Assign role to actor

Revoke role from actor

Grant permission to role

Revoke permission from role

Set user scope restriction

Remove user scope restriction

These operations behave as follows:

  • Calling link/assign/grant/add when the relationship already exists returns success without making changes

  • Calling unlink/revoke/remove when the relationship doesn't exist returns success without making changes

This design simplifies client code. You can safely retry these operations without worrying about conflicts or checking the current state first.

Best practices

  1. Always include the version in your queries. When fetching entities you plan to modify, request the version field so you have it ready for mutations.

  2. Handle conflicts gracefully. In collaborative applications, version conflicts are expected. Implement retry logic or prompt users to review changes.

  3. Don't cache versions long-term. Versions can change at any time. Always use the version from your most recent fetch of the entity.

  4. Consider user experience. When a conflict occurs, show users what changed and let them decide how to proceed rather than silently retrying.

Last updated

Was this helpful?