Session Token Cache With Per-Entry Expiry and a Test Suite
NetflixSoftware Engineer · Technical ScreenLast updated
Implement an in-memory cache whose entries expire individually, reclaim memory for keys that are never read again, and prove it with deterministic tests using an injected clock. Tests expiry semantics, stale-entry handling and test design.
A login service keeps short-lived session tokens in process memory. Every token should vanish a fixed time after it was issued, and a busy server must not leak memory on tokens nobody ever looks up again. Build the cache, talk through your design choices, and finish with a test suite you actually run.
Interface
class ExpiringCache:
def __init__(self, clock=time.monotonic): ...
def set(self, key: str, value: object, ttl: float) -> None: ...
def get(self, key: str, default=None) -> object: ...
def remove(self, key: str) -> bool: ...
def live_count(self) -> int: ...
def purge_expired(self) -> int: ...
setstoresvalueand makes it expirettlseconds from now. Writing to an existing key replaces both the value and the expiry.getreturns the value if the key is live, otherwisedefault.removedeletes the key and returnsTrueonly if a live entry was removed.live_countreturns the number of live entries.purge_expireddrops every expired entry and returns how many it removed.
Requirements
- Define the boundary. An entry written at time
wwith TTLdis live forw <= now < w + d. At exactlyw + dit is gone. - Inject the clock. Tests must never call
sleep. Pass a callable clock so tests can move time forward instantly. - Reclaim memory. Expired keys that are never read again must eventually be freed, without a full scan of the map on every call.
- Values may be
None. Callers who care can tell "storedNone" apart from "missing" by passing their owndefaultsentinel. - Single process, single thread for the base version. No persistence.
Example session
clock = 1000
set("s1", "tok-A", ttl=30) # expires at 1030
clock = 1029.9 -> get("s1") -> "tok-A"
clock = 1030 -> get("s1") -> None (expired at the boundary)
set("s2", "x", ttl=10) at 1030; set("s2", "y", ttl=10) at 1038
clock = 1043 -> get("s2") -> "y" (the overwrite reset the TTL)
What to deliver
- The implementation, with the time complexity of each operation.
- A short explanation of how expired entries are reclaimed, and the trade-offs of your approach.
- At least six focused unit tests: the boundary, overwrites, removal semantics,
Nonevalues, bulk reclamation, and input validation.
Hints
Hint 1
Pin down the boundary first: is an entry still live at exactly write_time + ttl?
Hint 2
Lazy deletion on read is correct but leaks memory for keys that are never read. What structure lets you find the soonest-expiring entries quickly?
Hint 3
A min-heap keyed by expiry works, but overwrites leave old heap rows behind. How will you recognize them?
Hint 4
Make time a constructor parameter so your tests can advance it instantly.
Follow-up questions
- Make the cache safe for concurrent use from many threads. Where do the locks go, and could you shard them?
- Add a maximum size with LRU eviction on top of TTL expiry.
- How would you support refresh-on-read (sliding expiration) efficiently?
- How would this change for a cache shared across many processes or hosts?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.