Implement an in-memory key-field-value DB with TTL
Company: Meta
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
Quick Answer: This question evaluates implementation skills in efficient in-memory data structures, time-to-live (TTL) expiration semantics, ordered and prefix scans, and snapshot backup/restore mechanisms.
Constraints
- key, field, prefix, and value are strings; value is treated as a string.
- timestamps and ttl are integers; ttl > 0.
- A (key, field) is visible at time t iff t < expire_at; a no-TTL set uses expire_at = +inf.
- delete removes the field regardless of expiry and returns whether it existed.
- restore selects the most recent backup with backup_time <= restore_to_time; if none, the DB becomes empty.
- Up to ~1e5 operations total.
Examples
Input: ([['set', 'k1', 'f1', 'v1', 1], ['get', 'k1', 'f1', 2], ['set_with_ttl', 'k1', 'f2', 'v2', 5, 10], ['get', 'k1', 'f2', 14], ['get', 'k1', 'f2', 15], ['get', 'k1', 'fX', 3], ['get', 'kX', 'f1', 3], ['delete', 'k1', 'f1', 20], ['delete', 'k1', 'f1', 21], ['get', 'k1', 'f1', 22]],)
Expected Output: [None, 'v1', None, 'v2', None, None, None, True, False, None]
Explanation: Basic set/get with a TTL expiry boundary (t>=expire is expired), missing key/field returns null, and delete returns true only when the field exists.
Input: ([['set', 'k', 'banana', 'B', 0], ['set', 'k', 'apple', 'A', 0], ['set_with_ttl', 'k', 'apricot', 'AP', 0, 5], ['set', 'k', 'cherry', 'C', 0], ['scan', 'k', 3], ['scan', 'k', 5], ['scan_with_prefix', 'k', 'ap', 3], ['scan_with_prefix', 'k', 'ap', 5], ['scan', 'missing', 0]],)
Expected Output: [None, None, None, None, [['apple', 'A'], ['apricot', 'AP'], ['banana', 'B'], ['cherry', 'C']], [['apple', 'A'], ['banana', 'B'], ['cherry', 'C']], [['apple', 'A'], ['apricot', 'AP']], [['apple', 'A']], []]
Explanation: scan and scan_with_prefix return [field, value] pairs sorted by field ascending, filtering out expired entries; a missing key yields an empty list.
Input: ([['set', 'k', 'f', 'v1', 1], ['backup', 10], ['set', 'k', 'f', 'v2', 11], ['backup', 20], ['set', 'k', 'f', 'v3', 21], ['get', 'k', 'f', 22], ['restore', 30, 25], ['get', 'k', 'f', 31], ['restore', 40, 15], ['get', 'k', 'f', 41], ['restore', 50, 5], ['get', 'k', 'f', 51]],)
Expected Output: [None, None, None, None, None, 'v3', None, 'v2', None, 'v1', None, None]
Explanation: backup/restore: restore loads the most recent backup whose backup_time <= restore_to_time; with no eligible backup the DB becomes empty.
Input: ([['set_with_ttl', 'a', 'x', '1', 0, 100], ['backup', 5], ['delete', 'a', 'x', 6], ['get', 'a', 'x', 7], ['restore', 8, 5], ['get', 'a', 'x', 9], ['get', 'a', 'x', 100], ['restore', 200, 1], ['scan', 'a', 201]],)
Expected Output: [None, None, True, None, None, '1', None, None, []]
Explanation: TTL and deletes are captured in snapshots: a deleted field comes back after restore with its original expiry intact; no eligible backup restores to empty.
Input: ([],)
Expected Output: []
Explanation: Edge case: an empty operation list returns an empty result list.
Hints
- Model storage as a nested map key -> field -> (value, expire_at). Store expire_at = +inf for set without TTL and timestamp + ttl for set_with_ttl, so get/scan only need the single check t < expire_at.
- Don't physically purge expired entries on a timer — just filter them at read time (get/scan). Deletion is the only thing that actually removes a field.
- For backup, store a DEEP copy of the whole storage map under its timestamp; a shallow copy would let later mutations corrupt the snapshot. For restore, keep backup timestamps sorted and binary-search for the largest one <= restore_to_time (and restore to empty when there is none).