In-Memory Key-Value Store with Transactions

Build an in-memory key-value store with set, get, delete and count-by-value, then nested transactions with begin, rollback and commit, then a sorted prefix scan.

Also known as: In-memory database, Simple database with rollback

Why practise this

In-memory database or key-value store shows up in 9.1% of the 1,447 coding interview reports we studied, at 8 companies. Build a small store with get, set and delete, then extend it with transactions, persistence to a file, filtering or TTLs.

Reported at: OpenAI, Databricks, Anthropic, Snowflake, xAI, Cursor, Perplexity, Zip.

This is an original practice problem written by AceOffer to train the pattern. It is not a question any company asked.

What candidates report interviewers add

Follow-ups candidates reported on questions with this pattern, in our words. Parts marked “from reports” below are built on them.

Design an in-memory store that maps string keys to string values and supports nested transactions.

Questions to ask before you code

  • With two levels open, should a commit merge the inner level into the outer level, or make everything permanent at once?
  • Is calling rollback with nothing open an error, or just a no-op that reports failure?
  • Should get see uncommitted writes?
  • How often is count called compared with writes? That decides whether a running tally is worth it.

Part 1: Basic operations

Implement KVStore with string keys and string values.

  • set(key, value) stores value under key, overwriting any earlier value.
  • get(key) returns the stored string, or None if the key is absent.
  • delete(key) removes the key and returns True; returns False if the key was absent.
  • count(value) returns how many keys currently map to value (0 if none).

count should be O(1): keep a running tally rather than scanning all keys.

Part 2: Nested transactionsfrom reports

Add transactions. They can be nested.

  • begin() opens a new transaction inside any that are already open.
  • Reads (get, count) always see the latest writes, including writes made in open transactions.
  • rollback() undoes every change made in the innermost open transaction, closes it, and returns True. Returns False if no transaction is open.
  • commit() closes the innermost open transaction and keeps its changes, returning True. If an outer transaction is still open, the changes become part of it, so rolling back the outer one also undoes them. Returns False if no transaction is open.

After a rollback, get and count must give the same answers as just before the matching begin(). That includes keys created inside the transaction: they must be absent again. count must stay O(1) and correct through rollbacks.

Part 3: Prefix scanfrom reports

Add scan(prefix), which returns every current key that starts with prefix, sorted in plain string order (so "user:10" comes before "user:2"). An empty prefix returns all keys; no match returns [].

scan sees the current state, so it reflects writes in open transactions and is restored by rollback like everything else.

Open this page on a laptop or desktop to write and run your code against the tests in the browser.

Approach

Spoilers: try each part before reading its walkthrough.

Part 1: Basic operations

Two dictionaries do all the work. data maps each key to its string, and counts maps each value to how many keys hold it right now. The trick is to send every mutation through one private helper, here _apply(key, value). It looks up the old value, lowers that value's tally, then writes the new value and raises its tally (or removes the key for a delete). Because no other code path touches data, the tally cannot drift, and count becomes a single dictionary lookup. Each operation is O(1) on average. The cost is a little extra memory, one entry per distinct value, which is the usual price for answering an aggregate query without a scan.

Part 2: Nested transactions

Keep a stack with one undo journal per open transaction. A journal is a dict from key to the value that key had before this transaction first changed it, with a private _MISSING marker for keys that did not exist. Only the first write to a key is recorded, so the journal always holds the pre-transaction value. rollback pops the journal and replays each saved value through the same _apply helper, which also repairs counts. commit pops the inner journal and merges it into the parent with setdefault, so the parent keeps its own older entries. With no parent, the changes are simply permanent. Rollback and nested commit cost O(k) for k keys touched, not O(n) for the whole store, which is why a journal beats copying the dictionary at every begin.

Part 3: Prefix scan

Because reads always go to the live data dictionary, a prefix scan needs no knowledge of transactions at all: filter keys with str.startswith and sort them. Rollback already restores data, so the scan is automatically consistent. This version is O(n log n) per call, which is fine for a small store. If scans dominate, keep a sorted list of keys and use bisect to jump to the first key at or after the prefix, then walk forward until keys stop matching; that is O(log n + m) per scan for m matches, at the cost of O(n) inserts. A trie gives similar scan speed with cheaper inserts but more memory.

Hints

Part 1 hint

Keep a second dict from value to how many keys hold it. Route every write through one helper that decrements the old value and increments the new one.

Part 2 hint

For each open transaction, remember a key's value from before its first change there (or a marker meaning 'was absent'). Rollback writes those old values back through the same helper, so count stays right.

Part 2 hint

On a nested commit, copy the inner undo entries into the outer one, but keep the outer entry when both have the key: it is the older value.

Common mistakes

  • Supporting only one open transaction at a time. Candidates report being caught out when the tests open a transaction inside another one.
  • Copying the whole store on every begin(), which costs O(n) per transaction.
  • Saving the old value on every write instead of only the first one in the transaction, so rollback restores a value from the middle of the transaction.
  • Using None both for 'absent' and as a saved value, which leaves keys behind after rollback.
  • Updating data on rollback but not the value counts.
  • Making commit discard all open transactions instead of just the innermost.

Where an interviewer could take it next

  • Make scan fast on a large store, for example with a sorted key list and bisect, or a trie.
  • Add a time-to-live per key.
  • Persist committed writes to an append-only log and replay it on start-up.

Complexity

set, get, delete and count are O(1). begin is O(1); rollback and nested commit are O(k) for the k keys changed in that transaction. scan is O(n log n) for n keys because it filters and sorts.

Frequently asked

How do nested transactions and rollback work in a key-value store?
Each begin pushes a new layer onto a stack. Writes are applied to the live data immediately, and the top layer records what each touched key looked like before. Rollback pops the top layer and restores those values. Commit pops the top layer and hands its saved values to the layer below, so an outer rollback can still undo everything. When no layer is left, the changes are permanent.
Should a transaction use an undo journal or a snapshot of the data?
A snapshot copies the whole store at begin, which is simple but costs O(n) time and memory for every transaction, even one that changes a single key. An undo journal stores only the old values of keys the transaction actually touches, so begin is O(1) and rollback is proportional to the work done. Interviewers usually expect the journal once nesting comes up.
How do you keep count(value) O(1) when transactions can be rolled back?
Maintain a dictionary from value to the number of keys holding it, and update it inside the single helper that performs every write. Rollback restores old values by calling that same helper rather than editing the data dictionary directly, so the tally is adjusted exactly like a normal write. The count then never needs a scan, before or after a rollback.

Practise next

See what candidates report at each company

The company guides collect the questions candidates report, with every part, the follow-ups interviewers added, and how often each one comes up.

Browse the interview guides
Is this helpful?