Implement a thread-safe high-concurrency LRU cache
Company: MongoDB
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This question evaluates understanding of concurrent data structures, synchronization strategies, and LRU cache semantics within the Coding & Algorithms domain, testing the ability to design an in-memory cache with O(1) average operations and explicit thread-safety guarantees.
Part 1: Basic O(1) LRU Cache Simulator
Constraints
- 1 <= capacity <= 10^5
- 0 <= key, value <= 10^9
- 0 <= len(operations) <= 2 * 10^5
- Each operation is either `('put', key, value)` or `('get', key)`
Examples
Input: (2, [('put', 1, 1), ('put', 2, 2), ('get', 1), ('put', 3, 3), ('get', 2), ('put', 4, 4), ('get', 1), ('get', 3), ('get', 4)])
Expected Output: [1, -1, -1, 3, 4]
Explanation: After get(1), key 1 becomes most recently used. Putting 3 evicts 2. Putting 4 later evicts 1. The get results are 1, -1, -1, 3, 4.
Input: (2, [('put', 2, 1), ('put', 2, 2), ('get', 2), ('put', 1, 1), ('put', 4, 1), ('get', 2)])
Expected Output: [2, -1]
Explanation: The second put updates key 2 to value 2. After inserting 1 and then 4, key 2 becomes the least recently used and is evicted. So the get results are 2 and then -1.
Input: (0, [('put', 1, 10), ('get', 1), ('put', 2, 20), ('get', 2)])
Expected Output: [-1, -1]
Explanation: A cache with capacity 0 cannot store anything, so every get returns -1.
Input: (1, [('put', -1, 5), ('get', -1), ('put', -1, 7), ('get', -1), ('put', 2, 9), ('get', -1), ('get', 2)])
Expected Output: [5, 7, -1, 9]
Explanation: Updating an existing key changes its value without increasing size. When key 2 is inserted into a capacity-1 cache, key -1 is evicted.
Hints
- A hash map gives O(1) access to a key, but you also need O(1) updates to recency order.
- A doubly linked list with dummy head/tail nodes makes it easy to move a node to the most-recently-used position and remove the least-recently-used node.
Part 2: High-Concurrency Sharded LRU Cache Simulator
Constraints
- 1 <= capacity <= 10^5
- 1 <= shard_count <= capacity
- 0 <= key, value <= 10^9
- 0 <= len(operations) <= 2 * 10^5
- Each operation is either `('put', key, value)` or `('get', key)`
Examples
Input: (4, 2, [('put', 1, 1), ('get', 1), ('put', 3, 3), ('put', 5, 5), ('get', 1), ('put', 1, 1), ('get', 1), ('get', 5)])
Expected Output: [1, -1, 1, 5]
Explanation: All keys map to shard 1, whose capacity is 2. Key 1 is evicted when key 5 is inserted, then reinserted later.
Input: (4, 3, [('put', 1, 20), ('put', 4, 40), ('get', 1), ('get', 4), ('put', 0, 10), ('put', 3, 30), ('get', 0), ('put', 6, 60), ('get', 6)])
Expected Output: [-1, 40, 10, 60]
Explanation: Shard capacities are [2, 1, 1]. Key 1 is evicted from shard 1 by key 4. In shard 0, getting key 0 makes it recent, so key 3 is evicted when key 6 is inserted.
Input: (2, 4, [('put', 2, 20), ('put', 1, 10), ('get', 2), ('get', 1), ('put', 5, 50), ('get', 1), ('get', 5)])
Expected Output: [-1, 10, -1, 50]
Explanation: Shard capacities are [1, 1, 0, 0]. Key 2 maps to a zero-capacity shard, so it is never stored. Keys 1 and 5 share shard 1, so 5 evicts 1.
Input: (0, 3, [('put', 0, 100), ('put', 1, 200), ('get', 0), ('get', 1)])
Expected Output: [-1, -1]
Explanation: Every shard has capacity 0, so all puts are ignored.
Input: (3, 1, [('put', 1, 1), ('put', 2, 2), ('put', 1, 10), ('put', 3, 3), ('put', 4, 4), ('get', 2), ('get', 1), ('get', 4)])
Expected Output: [-1, 10, 4]
Explanation: Updating key 1 changes its value and makes it most recently used. Later, key 2 becomes the least recently used and is evicted.
Input: (5, 2, [])
Expected Output: []
Explanation: No operations means no get results.
Hints
- Reuse the LRU data structure from Part 1, but create one independent instance per shard.
- Precompute each shard's capacity. The first `capacity % shard_count` shards get one extra slot.