Design a time-travel key-field store with TTL
Company: Tradedesk
Role: Machine Learning Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
Quick Answer: This question evaluates a candidate's understanding of time-versioned key-field storage, per-write TTL expirations, timestamp-based visibility, and atomic compare-and-set/delete semantics, exercising competency in maintaining historical state and correct value visibility over time.
Part 1: Basic Key-Field Store Operations
Constraints
- 1 <= len(operations) <= 200000
- Timestamps are non-decreasing integers encoded as strings.
- Keys and fields are non-empty lowercase strings of length at most 30.
- Values fit in a signed 32-bit integer.
- A compare operation against a missing field must fail.
Examples
Input: ([['SET', '1', 'user1', 'age', '30'], ['GET', '2', 'user1', 'age'], ['CAS', '3', 'user1', 'age', '30', '31'], ['GET', '4', 'user1', 'age'], ['CAD', '5', 'user1', 'age', '30'], ['CAD', '6', 'user1', 'age', '31'], ['GET', '7', 'user1', 'age']],)
Expected Output: ['30', 'true', '31', 'false', 'true', '']
Explanation: The value is set to 30, updated to 31 by a successful CAS, then deleted by a successful CAD.
Input: ([['GET', '1', 'k', 'f'], ['CAS', '2', 'k', 'f', '0', '1'], ['CAD', '3', 'k', 'f', '0']],)
Expected Output: ['', 'false', 'false']
Explanation: Missing fields read as empty output and cannot satisfy compare operations.
Input: ([['SET', '1', 'k1', 'a', '-5'], ['SET', '1', 'k1', 'b', '10'], ['SET', '2', 'k2', 'a', '7'], ['GET', '3', 'k1', 'a'], ['GET', '3', 'k2', 'a'], ['CAS', '4', 'k1', 'b', '10', '11'], ['GET', '5', 'k1', 'b'], ['GET', '5', 'k2', 'b']],)
Expected Output: ['-5', '7', 'true', '11', '']
Explanation: Different keys and fields are independent, and negative integer values are supported.
Input: ([['SET', '5', 'k', 'f', '1'], ['SET', '5', 'k', 'f', '2'], ['GET', '5', 'k', 'f'], ['CAD', '5', 'k', 'f', '1'], ['CAD', '5', 'k', 'f', '2'], ['GET', '5', 'k', 'f']],)
Expected Output: ['2', 'false', 'true', '']
Explanation: Operations with the same timestamp are still processed in input order.
Hints
- A nested dictionary key -> field -> value is enough for this part.
- Implement one helper-style lookup behavior and reuse it for GET, CAS, and CAD.
Part 2: Key-Field Store with Sorted Scans
Constraints
- 1 <= len(operations) <= 200000
- Timestamps are non-decreasing integers encoded as strings.
- Keys, fields, and prefixes contain lowercase letters; fields do not contain commas or parentheses.
- Values fit in a signed 32-bit integer.
- Scan output must be sorted by field name ascending.
Examples
Input: ([['SET', '1', 'profile', 'name', '5'], ['SET', '2', 'profile', 'age', '30'], ['SET', '3', 'profile', 'zip', '90210'], ['SCAN', '4', 'profile'], ['SCAN_PREFIX', '5', 'profile', 'z'], ['SCAN_PREFIX', '6', 'profile', 'na']],)
Expected Output: ['age(30),name(5),zip(90210)', 'zip(90210)', 'name(5)']
Explanation: SCAN sorts fields alphabetically. Prefix scans include only matching field names.
Input: ([['SCAN', '1', 'missing'], ['SCAN_PREFIX', '2', 'missing', 'a'], ['GET', '3', 'missing', 'f']],)
Expected Output: ['', '', '']
Explanation: Scanning or reading a missing key returns an empty output string.
Input: ([['SET', '1', 'k', 'apple', '1'], ['SET', '2', 'k', 'apricot', '2'], ['SET', '3', 'k', 'banana', '3'], ['CAD', '4', 'k', 'banana', '3'], ['SCAN_PREFIX', '5', 'k', 'ap'], ['CAS', '6', 'k', 'apple', '1', '9'], ['SCAN', '7', 'k']],)
Expected Output: ['true', 'apple(1),apricot(2)', 'true', 'apple(9),apricot(2)']
Explanation: Deleted fields disappear from scans, and successful CAS changes the displayed value.
Input: ([['SET', '1', 'k', 'x', '4'], ['SCAN_PREFIX', '2', 'k', '']],)
Expected Output: ['x(4)']
Explanation: An empty prefix matches every field.
Hints
- A scan is just a filtered view of the current dictionary for one key.
- Sort field names before formatting; test the prefix against the field name, not against the final formatted string.
Part 3: Key-Field Store with TTL Expiration
Constraints
- 1 <= len(operations) <= 200000
- Timestamps are non-decreasing integers encoded as strings.
- 1 <= ttl <= 1000000000 for TTL writes.
- Keys, fields, and prefixes contain lowercase letters; fields do not contain commas or parentheses.
- Values fit in a signed 32-bit integer.
Examples
Input: ([['SET_TTL', '1', 'k', 'f', '10', '5'], ['GET', '1', 'k', 'f'], ['GET', '5', 'k', 'f'], ['GET', '6', 'k', 'f']],)
Expected Output: ['10', '10', '']
Explanation: The value is visible at timestamps 1 through 5, but not at timestamp 6.
Input: ([['SET_TTL', '1', 'k', 'f', '1', '3'], ['GET', '3', 'k', 'f'], ['SET', '4', 'k', 'f', '2'], ['GET', '100', 'k', 'f']],)
Expected Output: ['1', '2']
Explanation: The TTL value expires at timestamp 4, then a permanent SET overwrites it.
Input: ([['SET_TTL', '1', 'k', 'f', '5', '2'], ['CAS', '3', 'k', 'f', '5', '6'], ['SET_TTL', '4', 'k', 'f', '7', '10'], ['CAS_TTL', '5', 'k', 'f', '7', '8', '2'], ['GET', '6', 'k', 'f'], ['GET', '7', 'k', 'f']],)
Expected Output: ['false', 'true', '8', '']
Explanation: The first CAS fails because the old value expired. CAS_TTL then writes 8, which expires at timestamp 7.
Input: ([['SET_TTL', '1', 'k', 'a', '1', '10'], ['SET_TTL', '2', 'k', 'b', '2', '3'], ['SET', '3', 'k', 'c', '3'], ['SCAN', '4', 'k'], ['SCAN', '5', 'k'], ['SCAN_PREFIX', '6', 'k', 'b']],)
Expected Output: ['a(1),b(2),c(3)', 'a(1),c(3)', '']
Explanation: Field b expires at timestamp 5, so it is absent from later scans.
Input: ([['SET_TTL', '10', 'k', 'f', '99', '1'], ['GET', '10', 'k', 'f'], ['GET', '11', 'k', 'f'], ['CAD', '12', 'k', 'f', '99']],)
Expected Output: ['99', '', 'false']
Explanation: A ttl of 1 makes the value visible only at its write timestamp; after expiration, CAD fails.
Hints
- Store an expiration time together with each current value. Permanent writes can use a very large expiration time.
- The interval is half-open: a value with write time t and ttl is already expired at timestamp t + ttl.
Part 4: Time-Travel Key-Field Store with TTL
Constraints
- 1 <= len(operations) <= 200000
- Timestamps are non-decreasing integers encoded as strings.
- For GET_WHEN, at_timestamp <= timestamp.
- 1 <= ttl <= 1000000000 for TTL writes.
- If multiple updates to the same field have the same timestamp, later commands in the input are considered later for subsequent queries at that timestamp.
- Keys, fields, and prefixes contain lowercase letters; fields do not contain commas or parentheses.
Examples
Input: ([['SET', '1', 'k', 'f', '10'], ['SET', '3', 'k', 'f', '20'], ['GET_WHEN', '4', 'k', 'f', '2'], ['GET_WHEN', '4', 'k', 'f', '3'], ['GET', '4', 'k', 'f'], ['CAD', '5', 'k', 'f', '20'], ['GET_WHEN', '6', 'k', 'f', '4'], ['GET_WHEN', '6', 'k', 'f', '5'], ['GET', '6', 'k', 'f']],)
Expected Output: ['10', '20', '20', 'true', '20', '', '']
Explanation: Historical queries before the deletion still see 20; queries at or after the deletion see missing.
Input: ([['SET_TTL', '10', 'k', 'f', '7', '5'], ['GET_WHEN', '12', 'k', 'f', '10'], ['GET_WHEN', '16', 'k', 'f', '14'], ['GET_WHEN', '16', 'k', 'f', '15'], ['GET', '16', 'k', 'f'], ['SET', '20', 'k', 'f', '9'], ['GET_WHEN', '21', 'k', 'f', '16'], ['GET_WHEN', '21', 'k', 'f', '20']],)
Expected Output: ['7', '7', '', '', '', '9']
Explanation: The TTL write is visible for times 10 through 14 only. A later permanent write does not change history at timestamp 16.
Input: ([['SET', '1', 'k', 'a', '1'], ['CAS_TTL', '2', 'k', 'a', '1', '2', '3'], ['GET_WHEN', '4', 'k', 'a', '1'], ['GET_WHEN', '4', 'k', 'a', '2'], ['GET_WHEN', '5', 'k', 'a', '5'], ['CAS', '5', 'k', 'a', '2', '3'], ['SET_TTL', '6', 'k', 'a', '4', '10'], ['CAD', '7', 'k', 'a', '4'], ['GET_WHEN', '8', 'k', 'a', '6'], ['GET_WHEN', '8', 'k', 'a', '7']],)
Expected Output: ['true', '1', '2', '', 'false', 'true', '4', '']
Explanation: CAS_TTL creates a historical TTL version. The CAS at timestamp 5 fails because that version has expired. The later delete is represented in history.
Input: ([['SET_TTL', '1', 'k', 'b', '2', '5'], ['SET', '2', 'k', 'a', '1'], ['SCAN', '3', 'k'], ['SCAN', '6', 'k'], ['GET_WHEN', '7', 'k', 'b', '5'], ['GET_WHEN', '7', 'k', 'b', '6'], ['SCAN_PREFIX', '7', 'k', 'b']],)
Expected Output: ['a(1),b(2)', 'a(1)', '2', '', '']
Explanation: Current scans respect TTL expiration, while GET_WHEN can still inspect earlier visible times.
Input: ([['GET_WHEN', '1', 'missing', 'f', '1'], ['SET', '2', 'k', 'f', '1'], ['SET', '2', 'k', 'f', '2'], ['GET_WHEN', '2', 'k', 'f', '2'], ['CAD', '2', 'k', 'f', '2'], ['GET_WHEN', '2', 'k', 'f', '2']],)
Expected Output: ['', '2', 'true', '']
Explanation: This edge case covers missing history and multiple updates with the same timestamp processed in order.
Hints
- Keep an append-only version list for each key-field pair. Include tombstone versions for successful deletes.
- For a historical read, binary search for the last version with write_timestamp <= at_timestamp, then check whether it was a deletion or expired by at_timestamp.