Design an in-memory database with TTL and history
Company: Meta
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Take-home Project
Quick Answer: This question evaluates understanding of time-versioned in-memory key–field–value stores, covering TTL-based expiration, historical reads, atomic compare-and-set/delete semantics, efficient scan/scan-by-prefix operations, and time/space complexity analysis, and it belongs to the Coding & Algorithms domain.
Constraints
- Keys, fields, and prefixes are non-empty strings; values and ttl are integers with ttl > 0.
- Timestamps passed to successive calls are strictly increasing.
- For getWhen, atTimestamp <= timestamp (you only ever query the past or present).
- A value set with ttl is live on [timestamp, timestamp + ttl); it is expired at exactly timestamp + ttl.
- scan / scanByPrefix output is sorted alphabetically by field name and excludes expired or deleted fields.
- 1 <= number of operations <= 10^4.
Examples
Input: [['set', 1, 'user1', 'age', 12], ['get', 2, 'user1', 'age'], ['get', 3, 'user1', 'height'], ['get', 4, 'userX', 'age'], ['compareAndSet', 5, 'user1', 'age', 12, 13], ['get', 6, 'user1', 'age'], ['compareAndSet', 7, 'user1', 'age', 99, 100], ['compareAndDelete', 8, 'user1', 'age', 13], ['get', 9, 'user1', 'age'], ['compareAndDelete', 10, 'user1', 'age', 13]]
Expected Output: [None, 12, None, None, True, 13, False, True, None, False]
Explanation: Level 1: set stores 12; get returns it; missing field/key give null. compareAndSet succeeds when current==expected (12->13) and fails otherwise (current 13 != 99). compareAndDelete removes the field when it matches (13), so the next get is null and the second delete fails.
Input: [['set', 1, 'user2', 'age', 18], ['set', 2, 'user2', 'height', 180], ['set', 3, 'user2', 'address', 1], ['scan', 4, 'user2'], ['scanByPrefix', 5, 'user2', 'a'], ['scan', 6, 'nope']]
Expected Output: [None, None, None, ['address(1)', 'age(18)', 'height(180)'], ['address(1)', 'age(18)'], []]
Explanation: Level 2: scan lists every live field as 'field(value)' sorted alphabetically (address, age, height). scanByPrefix 'a' keeps only address and age. Scanning a missing key returns an empty array.
Input: [['setWithTtl', 10, 'k', 'f', 5, 5], ['get', 12, 'k', 'f'], ['get', 15, 'k', 'f'], ['get', 20, 'k', 'f'], ['scan', 16, 'k']]
Expected Output: [None, 5, None, None, []]
Explanation: Level 3: setWithTtl at t=10 with ttl=5 makes the value live on [10,15). get at 12 sees 5; at 15 (the expiry boundary, exclusive) and at 20 it is gone. scan at 16 omits the expired field, returning [].
Input: [['setWithTtl', 10, 'k', 'f', 10, 2], ['set', 18, 'k', 'f', 18], ['getWhen', 30, 'k', 'f', 5], ['getWhen', 30, 'k', 'f', 11], ['getWhen', 30, 'k', 'f', 15], ['getWhen', 30, 'k', 'f', 20]]
Expected Output: [None, None, None, 10, None, 18]
Explanation: Level 4 (the prompt's worked example): value 10 lives on [10,12), value 18 lives on [18,inf). getWhen(5)=null (before any write), getWhen(11)=10, getWhen(15)=null (10 expired, 18 not yet written), getWhen(20)=18.
Input: [['set', 1, 'a', 'x', 5], ['compareAndSetWithTtl', 2, 'a', 'x', 5, 9, 3], ['get', 3, 'a', 'x'], ['get', 5, 'a', 'x'], ['get', 6, 'a', 'x']]
Expected Output: [None, True, 9, None, None]
Explanation: compareAndSetWithTtl: current value 5 matches expected, so it writes 9 with ttl=3 -> live on [2,5). get at 3 returns 9; at 5 (exclusive expiry) and 6 the value has expired.
Input: []
Expected Output: []
Explanation: Edge case: no operations yields an empty result list.
Hints
- Model the store as key -> field -> list of versions, where each version is (start, end, value). end is the expiry timestamp (writeTime + ttl) or infinity for a non-TTL value.
- Because timestamps strictly increase, a new write to an existing field can simply close the previous live version by setting its end to the new write time; compareAndDelete closes the current version at the delete time.
- For get / scan, the relevant version is the latest one whose interval [start, end) contains the query timestamp. For getWhen(atTimestamp), binary-search the version list for the interval covering atTimestamp.
- Treat expiry as exclusive: a value with end = E is visible for t < E and gone at t == E. This is exactly why getWhen(15) is null in the worked example (10 expired at 12, 18 not written until 18).