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
keydoesn't exist andexpectedVersionis0, create it with version1and return1. - If
keyexists and its version equalsexpectedVersion, storevalue, increment the version and return the new version. - Otherwise return
-1(conflict: someone else saved first, or the key doesn't exist).
- If
get(key)returns the current value, ornullif missing.version(key)returns the current version, or0if 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;
}
}
class VersionedStore {
private rows = new Map<string, { value: string; version: number }>();
update(key: string, value: string, expectedVersion: number): number {
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: string): string | null {
return this.rows.get(key)?.value ?? null;
}
version(key: string): number {
return this.rows.get(key)?.version ?? 0;
}
}
class VersionedStore:
def __init__(self):
self.rows: dict[str, list] = {} # key -> [value, version]
def update(self, key: str, value: str, expected_version: int) -> int:
row = self.rows.get(key)
if row is None:
if expected_version != 0:
return -1
self.rows[key] = [value, 1]
return 1
if row[1] != expected_version:
return -1
row[0] = value
row[1] += 1
return row[1]
def get(self, key: str) -> str | None:
row = self.rows.get(key)
return row[0] if row else None
def version(self, key: str) -> int:
row = self.rows.get(key)
return row[1] if row else 0
<?php
class VersionedStore
{
/** @var array<string, array{value: string, version: int}> */
private array $rows = [];
public function update(string $key, string $value, int $expectedVersion): int
{
if (!isset($this->rows[$key])) {
if ($expectedVersion !== 0) {
return -1;
}
$this->rows[$key] = ['value' => $value, 'version' => 1];
return 1;
}
if ($this->rows[$key]['version'] !== $expectedVersion) {
return -1;
}
$this->rows[$key]['value'] = $value;
return ++$this->rows[$key]['version'];
}
public function get(string $key): ?string
{
return $this->rows[$key]['value'] ?? null;
}
public function version(string $key): int
{
return $this->rows[$key]['version'] ?? 0;
}
}
class VersionedStore {
private static final class Row { String value; int version; Row(String v) { value = v; version = 1; } }
private final Map<String, Row> rows = new HashMap<>();
public VersionedStore() {}
public int update(String key, String value, int expectedVersion) {
Row row = rows.get(key);
if (row == null) {
if (expectedVersion != 0) return -1;
rows.put(key, new Row(value));
return 1;
}
if (row.version != expectedVersion) return -1;
row.value = value;
return ++row.version;
}
public String get(String key) {
Row row = rows.get(key);
return row == null ? null : row.value;
}
public int version(String key) {
Row row = rows.get(key);
return row == null ? 0 : row.version;
}
}
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.
More Design & Data Structures problems
- LRU Cache · Medium
- Token Bucket Rate Limiter · Medium
- Key-Value Store with TTL · Medium
- URL Shortener Codes (Base62) · Easy
- Build an EventEmitter · Medium
- Row Lock Manager with Deadlock Detection · Hard