Katkat

Session Token Cache With Per-Entry Expiry and a Test Suite

NetflixSoftware Engineer · Technical Screen

Last 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: ...
  • set stores value and makes it expire ttl seconds from now. Writing to an existing key replaces both the value and the expiry.
  • get returns the value if the key is live, otherwise default.
  • remove deletes the key and returns True only if a live entry was removed.
  • live_count returns the number of live entries.
  • purge_expired drops every expired entry and returns how many it removed.

Requirements

  1. Define the boundary. An entry written at time w with TTL d is live for w <= now < w + d. At exactly w + d it is gone.
  2. Inject the clock. Tests must never call sleep. Pass a callable clock so tests can move time forward instantly.
  3. Reclaim memory. Expired keys that are never read again must eventually be freed, without a full scan of the map on every call.
  4. Values may be None. Callers who care can tell "stored None" apart from "missing" by passing their own default sentinel.
  5. 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, None values, 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?