Katkat

Design a Transactional Key-Value Store

Applied IntuitionSoftware Engineer · Technical Screen

Last updated

This problem tests the ability to design an in-memory key-value store that supports nested transactions, including atomic commits, rollbacks, and precise state management across layers.

Design a Transactional Key-Value Store

Design an in-memory key-value store class TransactionStore that supports standard CRUD operations alongside transaction management. The store must support nested transactions, where changes made within a sub-transaction are only visible to the parent transaction until committed.

Requirements

  1. put(key, value): Associates a value with a key. If a transaction is active, the change must be scoped to the current transaction.
  2. get(key): Returns the value associated with the key. It must reflect the most recent state visible to the current transaction (checking the current scope, then parent scopes, then the global store).
  3. delete(key): Removes the key. If inside a transaction, the deletion must be tracked so it is effectively hidden or removed upon commit.
  4. begin(): Starts a new transaction scope. Transactions can be nested.
  5. commit(): Flushes changes from the current scope into the parent scope. If at the root level, changes are finalized.
  6. rollback(): Discards all changes made in the current transaction scope.

Constraints

  • The store should handle arbitrary nesting levels.
  • A get operation must return a special indicator (e.g., None) if the key does not exist or has been deleted in the current scope.

Example

store = TransactionStore()
store.put("a", 1)
store.begin()
store.put("a", 2)
print(store.get("a")) # Expected: 2
store.rollback()
print(store.get("a")) # Expected: 1

Edge Cases

  • Deleting a key that was added in a parent transaction.
  • Committing an empty transaction.
  • Calling rollback() at the base level (should be ignored or raise an error).

Hints

Hint 1

Consider using a stack of dictionaries to represent each transaction layer, where the top of the stack is the current scope.

Hint 2

For the 'delete' operation, use a special 'tombstone' value or a separate set to distinguish between a key that doesn't exist and a key that has been explicitly deleted within the current transaction.

Hint 3

When a transaction commits, merge the top dictionary into the dictionary immediately below it in the stack.

Follow-up questions

  • How would you optimize the memory usage if the store contains millions of keys but transactions only touch a small subset?
  • How would this implementation change if you needed to support multi-threaded access to the store?
  • If this were to be persisted to disk, how would you implement a Write-Ahead Log (WAL) to ensure durability in case of a crash?