Katkat

Implement a Key-Value Cache with Per-Entry Expiration

NetflixSoftware Engineer · Technical Screen

Last updated

This problem tests the ability to design an in-memory storage system that combines fixed-capacity constraints with time-based invalidation logic.

Problem Statement

Design a thread-safe ExpiringCache that stores key-value pairs with a maximum capacity. Each entry in the cache has a specific Time-To-Live (TTL). Once an entry expires, it should no longer be accessible. If the cache reaches its capacity, it must evict the oldest entry currently in the cache (Least Recently Added policy).

Your implementation must support the following interface:

class ExpiringCache:
    def __init__(self, capacity: int):
        # Initialize the cache with a fixed max capacity

    def put(self, key: str, value: any, ttl_seconds: int) -> None:
        # Store value with a TTL; evict oldest if at capacity

    def get(self, key: str) -> any:
        # Return value if key exists and not expired, else return None

Requirements

  1. Lazy Expiration: If get() is called on an expired key, return None and remove the item.
  2. Eviction: When adding a new item exceeds capacity, remove the entry that was added the earliest (regardless of its expiration status).
  3. Concurrency: Ensure that multiple threads can safely read and write to the cache.

Examples

cache = ExpiringCache(capacity=2)
cache.put("a", 1, 10) # Cache: {"a": 1}
cache.put("b", 2, 5)  # Cache: {"a": 1, "b": 2}
# After 6 seconds:
cache.get("b")        # Returns None (expired)
cache.put("c", 3, 10) # Cache: {"a": 1, "c": 3} ("b" was evicted by capacity logic)

Edge Cases

  • Accessing a key exactly at its expiration time.
  • Repeatedly calling put with the same key.
  • Inserting into a cache with capacity=0 or 1.

Hints

Hint 1

Use a Hash Map for O(1) access and a Doubly Linked List to keep track of the insertion order for O(1) eviction.

Hint 2

Store the absolute expiration timestamp (current time + TTL) alongside the value to check for validity during retrieval.

Hint 3

Use a ReentrantLock (or similar concurrency primitive) to wrap internal operations and ensure thread safety.

Follow-up questions

  • How would you implement a proactive cleanup mechanism (e.g., a background thread) to remove expired items before they are accessed?
  • If the cache needed to be distributed across multiple servers, what trade-offs would you face regarding consistency and latency?
  • How would you change the eviction policy if you wanted to evict the Least Recently Used (LRU) entry instead of the oldest insertion?