Implement KV Store and Token Bucket
Company: Dropbox
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This question evaluates a candidate's skills in implementing core data structures and concurrent algorithms, specifically in-memory key-value store design, API considerations, time-based token-bucket rate limiting, thread-safety, and synchronization.
In-Memory Key-Value Store
Constraints
- 0 <= number of operations <= 10^5
- Each operation is one of put / get / delete
- Keys and values can be any hashable / storable type
- get on a missing key returns None; delete on a missing key returns False
Examples
Input: ([['put', 'a', 1], ['get', 'a'], ['put', 'a', 2], ['get', 'a'], ['delete', 'a'], ['get', 'a']],)
Expected Output: [None, 1, None, 2, True, None]
Explanation: put a=1, read 1, overwrite a=2, read 2, delete a returns True, then get a returns None (gone).
Input: ([['get', 'missing'], ['delete', 'missing']],)
Expected Output: [None, False]
Explanation: Reading and deleting an absent key returns None and False respectively.
Input: ([],)
Expected Output: []
Explanation: No operations -> empty result list.
Input: ([['put', 'x', 'hello'], ['put', 'y', 'world'], ['get', 'x'], ['get', 'y'], ['delete', 'x'], ['get', 'x'], ['delete', 'x']],)
Expected Output: [None, None, 'hello', 'world', True, None, False]
Explanation: Two keys stored; deleting x once returns True, a second delete returns False.
Input: ([['put', 'k', 0], ['put', 'k', 9], ['get', 'k']],)
Expected Output: [None, None, 9]
Explanation: Overwriting a key keeps only the latest value (9).
Hints
- A dictionary (hash map) gives expected O(1) put, get, and delete.
- Distinguish 'key exists with value None' edge cases by using membership (`in`) for delete rather than truthiness.
- delete should report whether the key was actually present so callers can detect no-ops.
Token-Bucket Rate Limiter
Constraints
- 1 <= capacity <= 10^9
- 0 < refill_rate (tokens per second)
- requests sorted by non-decreasing timestamp (seconds, >= 0)
- Bucket starts full; tokens never exceed capacity
- 0 <= n <= capacity per request
Examples
Input: (10, 1, [[0, 5], [0, 5], [0, 1]])
Expected Output: [True, True, False]
Explanation: Full bucket of 10: spend 5 then 5 (now 0); the third request for 1 token is rejected since no refill time elapsed.
Input: (10, 2, [[0, 10], [0, 1], [2, 4], [2, 1]])
Expected Output: [True, False, True, False]
Explanation: Drain all 10 at t=0; next request rejected. By t=2, 2s*2/s = 4 tokens refilled, so a request for 4 succeeds; the following request for 1 fails (bucket empty).
Input: (5, 1, [[0, 5], [100, 5]])
Expected Output: [True, True]
Explanation: Drain at t=0; by t=100 the bucket has long since refilled and capped at capacity 5, so 5 is available again.
Input: (3, 1, [[0, 3], [1, 1], [1, 1]])
Expected Output: [True, True, False]
Explanation: Drain 3 at t=0; 1s later exactly 1 token refilled and consumed; a second request at the same instant has nothing left.
Input: (10, 1, [])
Expected Output: []
Explanation: No requests -> empty result.
Input: (2, 5, [[0, 2], [0, 1], [10, 2]])
Expected Output: [True, False, True]
Explanation: Capacity caps refill: even after 10s * 5/s = 50 tokens would accrue, the bucket holds at most 2, so the final request for 2 succeeds.
Input: (4, 1, [[0, 0], [0, 4], [0, 0]])
Expected Output: [True, True, True]
Explanation: Requests for 0 tokens are always allowed; the middle request drains the full bucket of 4.
Hints
- Track current token count and the timestamp of the last refill; lazily refill on each request instead of on a timer.
- Refill amount is elapsed_seconds * refill_rate, but clamp the total with min(capacity, ...) so the bucket never overflows.
- Only deduct tokens when the request is allowed; a rejected request must leave the bucket untouched. In a real multithreaded version, wrap the refill+check+deduct in a single critical section (mutex) or a CAS retry loop.