Katkat

Design SnapshotMap with Sparse Histories and Constant-Time Snapshots

SnorkelSoftware Engineer · Technical Screen

Last updated

Design an in-memory key-value store supporting point-in-time snapshots, sparse version histories, binary-search historical reads, tombstones, and write coalescing.

Problem Statement

Design an in-memory key-value map, SnapshotMap, that supports constant-time snapshots and historical point-in-time queries.

To make storage scalable across long-lived processes, the data structure must maintain sparse histories: keys that are not modified in a snapshot period must not duplicate memory. In addition, consecutive modifications or deletions to the same key within the same snapshot window must be coalesced into a single historical record.

Implement the SnapshotMap class with the following methods:

  • __init__(): Initializes the map. The initial active snapshot ID is 0.
  • put(key: str, val: int) -> None: Sets the key to val for the current active snapshot. Overwrites any previous uncommitted write in the current snapshot.
  • get(key: str) -> Optional[int]: Returns the latest value of key. Returns None if the key does not exist or was deleted.
  • delete(key: str) -> None: Removes key in the current active snapshot by writing a tombstone. If the key already does not exist, this is a no-op.
  • take_snapshot() -> int: Freezes the current state under the current snapshot ID and advances the active snapshot ID by 1. Returns the frozen snapshot ID.
  • get_snapshot(key: str, snap_id: int) -> Optional[int]: Returns the value of key as of snap_id. Returns None if the key did not exist or was marked deleted at that snapshot. Raises ValueError if snap_id is invalid (i.e., = current active snapshot ID).

Complexity Requirements

  • take_snapshot: O(1) time.
  • put, delete: O(1) amortized time.
  • get: O(1) average time.
  • get_snapshot: O(log H_k) time, where H_k is the number of distinct snapshot versions recorded for key.
  • Space: O(K + U), where K is the number of distinct keys and U is the total number of unique (key, snap_id) modification pairs.

Example

sm = SnapshotMap()
sm.put("a", 10)
sm.put("b", 20)

# Coalescing test: overwrite "a" before taking snapshot 0
sm.put("a", 15)

snap0 = sm.take_snapshot()  # returns 0; captures {"a": 15, "b": 20}

sm.put("a", 30)
sm.delete("b")
snap1 = sm.take_snapshot()  # returns 1; captures {"a": 30, "b": None}

sm.get("a")                       # returns 30
sm.get("b")                       # returns None
sm.get_snapshot("a", snap0)       # returns 15
sm.get_snapshot("b", snap0)       # returns 20
sm.get_snapshot("b", snap1)       # returns None (tombstone)
sm.get_snapshot("c", snap0)       # returns None (never set)

Edge Cases to Consider

  • Querying a snap_id that does not exist yet (should raise an exception or handle out-of-range cleanly).
  • A key deleted and recreated across different snapshots.
  • Multiple delete calls or put followed by delete in the same snapshot epoch.
  • Keys that were never modified after snapshot 0 must correctly read from their initial entry in future snapshots.

Hints

Hint 1

Track a global `current_snap_id` and map each key to a list of historical modifications containing `(snap_id, value_or_tombstone)`.

Hint 2

For write coalescing, inspect the last entry in a key's history list; if its recorded snapshot matches `current_snap_id`, overwrite it in place instead of appending.

Hint 3

To resolve `get_snapshot(key, snap_id)`, perform a binary search (e.g., `bisect_right`) over the key's history to locate the largest recorded snapshot ID less than or equal to `snap_id`.

Follow-up questions

  • How would you implement a `compact(retention_snap_id)` method to reclaim memory from history entries older than a specified snapshot ID?
  • How does the binary search approach compare to using persistent data structures (e.g., copy-on-write trees) if reads need to be `O(1)` instead of `O(log H_k)`?
  • How would you ensure thread safety for concurrent writers and readers while keeping `take_snapshot()` non-blocking?