Coding problem · Easy · Design & Data Structures

Versioned Store with Optimistic Locking

Easy Practical tasks

Two merchandisers edit the same product in an admin panel; the second save silently reverts the first. Implement the optimistic locking layer that prevents it (the logic behind UPDATE … WHERE id = ? AND version = ?, JPA @Version, and HTTP If-Match).

  • update(key, value, expectedVersion):
    • If key doesn't exist and expectedVersion is 0, create it with version 1 and return 1.
    • If key exists and its version equals expectedVersion, store value, increment the version and return the new version.
    • Otherwise return -1 (conflict: someone else saved first, or the key doesn't exist).
  • get(key) returns the current value, or null if missing.
  • version(key) returns the current version, or 0 if missing.

See Optimistic Locking with Version Columns.

Example 1

Input: ["VersionedStore","update","get","version","update","update","get","version"]
[[],["p1","Old",0],["p1"],["p1"],["p1","Fixed",1],["p1","Price25",1],["p1"],["p1"]]
Output: [null,1,"Old",1,2,-1,"Fixed",2]
Explanation: The second editor still had version 1, so their save is rejected instead of overwriting.

Hints

Hint 1

Store (value, version) per key.

Hint 2

Create only when the caller says it expects a new record (version 0).

Write and run your solution

Loading the editor… (JavaScript is required to run code in your browser.)

Solution and explanation

Show the solution

Time O(1) per operation · Space O(keys)

Each record carries a version that increases on every successful write. A writer must say which version it read; if the stored version differs, someone else committed in between and the write is refused — the caller reloads and merges (for human edits) or retries (for computed updates). In SQL this is one statement: UPDATE products SET …, version = version + 1 WHERE id = ? AND version = ? with zero affected rows meaning conflict.

Reference solution

class VersionedStore {
  constructor() {
    this.rows = new Map(); // key -> { value, version }
  }

  update(key, value, expectedVersion) {
    const row = this.rows.get(key);
    if (!row) {
      if (expectedVersion !== 0) return -1;
      this.rows.set(key, { value, version: 1 });
      return 1;
    }
    if (row.version !== expectedVersion) return -1;
    row.value = value;
    row.version += 1;
    return row.version;
  }

  get(key) {
    return this.rows.get(key)?.value ?? null;
  }

  version(key) {
    return this.rows.get(key)?.version ?? 0;
  }
}

Common mistakes

  • Creating a record when the caller expected an existing one.
  • Incrementing the version on failed updates.
  • Allowing expectedVersion 0 to overwrite an existing record.

Follow-up questions

  • Map conflicts to HTTP 412 with ETag/If-Match.
  • Return the current value with the conflict so the client can merge.