AlgoMaster Logo

Design a Versioned Value Store

hard9 min readUpdated August 31, 2026
Listen to this chapter
Unlock Audio

Design a thread-safe store that supports both optimistic and pessimistic updates to one integer value.

Implement the VersionedValueStore class:

  • VersionedValueStore(initialValue) initializes the value and sets its version to 0.
  • read() atomically returns [value, version] from one consistent snapshot.
  • tryUpdate(expectedVersion, update) performs an optimistic update.
  • updateExclusive(update) performs a pessimistic, exclusive update.

For tryUpdate(expectedVersion, update):

  1. Atomically check the current version and read the current value.
  2. If the version is already different from expectedVersion, return false without invoking update.
  3. Invoke update(currentValue) without holding the store's lock.
  4. Commit the returned value and increment the version only if the version is still expectedVersion; otherwise, discard the computed value and return false.

The optimistic callback is invoked at most once. It may be invoked even when the method ultimately returns false because another update can commit while the callback is running. The callback must therefore be safe to run without committing its result.

For updateExclusive(update), hold the store's exclusive lock while invoking update(currentValue), then store the returned value and increment the version. Other operations on the same instance must not observe or modify the store until this callback finishes.

Every successful update increments the version exactly once. If either callback throws an exception, propagate the exception and leave both the value and version unchanged. Separate store instances must be independent.

The judge supplies all callbacks. A callback does not call another method on the same store. Standard concurrency, callback, collection, lock, and thread APIs are preloaded, so you do not need import, include, package, or using statements.

Example 1:

Input:

Output:

Explanation: The first optimistic update commits at version 0. The second uses a stale expected version, so its callback is not invoked. The exclusive update then changes 15 to 30.

Example 2:

Input:

Output:

Explanation: Both callbacks may compute from the initial value, but only one result can commit against version 0.

Constraints

  • -10^9 <= initialValue <= 10^9
  • Every value returned by a judge callback fits in a signed 32-bit integer.
  • The version fits in a signed 32-bit integer.
  • At most 10000 operations are performed on one instance.
  • Judge callbacks do not recursively call the same store.
  • The same instance may be accessed concurrently by multiple threads.

Starter Code

Understanding the Problem

The store's state is the pair (value, version). These fields must always be read and changed together. Protecting only the value or reading the fields separately can produce a snapshot that never actually existed.

The two update methods differ in where the potentially expensive callback runs:

  • An optimistic update reads a snapshot, releases the lock, computes a candidate value, and later validates the version before committing.
  • An exclusive update keeps the lock throughout the callback and commit, preventing conflicting work but blocking every other operation on that store.

This is the central tradeoff between the two locking styles. Optimistic work allows overlap but can be discarded. Pessimistic work cannot conflict, but it serializes access for the duration of the callback.

Optimistic Update Protocol

An optimistic update needs two short critical sections.

In the first section, compare the version with expectedVersion and copy the current value. A mismatch proves that the caller's view is stale, so return immediately without running the callback.

Run the callback after releasing the lock. Then enter the second critical section and compare the version again. The version changes on every successful update, so equality proves that no operation committed since the snapshot was taken. Commit the candidate and increment the version. If the comparison fails, discard the candidate.

Do not automatically retry the callback. Retrying would change the method's single-attempt semantics and could invoke user code more than once.

Exclusive Update Protocol

An exclusive update acquires the lock before reading the value and keeps it until after the new value and incremented version are stored. This ensures that its callback sees the latest value and that no optimistic or exclusive commit can interleave with it.

Compute into a local variable before changing either field. If the callback throws, normal lock cleanup releases the lock while the assignments are skipped, preserving the original state.

Each store owns its own lock. A static or global lock would unnecessarily make independent instances block one another.

Correctness

read holds the lock while copying both fields, so every returned array represents one committed state.

For tryUpdate, a successful second version check proves that the candidate was computed from the current value and that no intervening update committed. The value and version are then changed together under the lock. If another operation commits first, its version increment makes the check fail, preventing a lost update.

updateExclusive holds the same lock for its entire read-compute-write sequence. Therefore, exclusive updates cannot overlap, and every one applies to the result of all earlier successful updates.

Because assignments occur only after a callback returns normally, a thrown exception cannot partially update the state.

Code

Complexity Analysis

Let U be the time taken by the supplied callback.

  • read: O(1) time and O(1) auxiliary space.
  • tryUpdate: O(U) time and O(1) auxiliary space; its lock is held only for O(1) work.
  • updateExclusive: O(U) time and O(1) auxiliary space; its lock is held for the full O(U) callback.