Function Call LRU Cache
Instructions
Implement a class that memoizes calls to a function using an LRU cache, built up over two levels:
- Cache key — turn each call's arguments into a hashable key so repeated calls return the cached result instead of recomputing.
- Disk persistence — make the cache survive across process restarts using an append-only log.
The LRU mechanics (capacity limit, eviction, and recency updates) are already implemented for you. You may assume that every argument and every return value is JSON-serializable, and that every decorated function has a unique name.
Interfaces
class FunctionCache:
def __init__(self, func, limit: int): ...
def _cache_key(self, func_name: str, args: list): ... # build a hashable cache key
def get(self, *args): ... # cached call: hit refreshes recency, miss computes
Level 1: Build the Cache Key
Implement _cache_key(self, func_name, args) so that it returns a value usable as a dictionary key for the given call.
Hints
- The returned key must be hashable. The raw args list can't be used as a dict key. What if the arguments may themselves contain nested lists or dicts?
- The key must uniquely identify the
(func_name, args)pair, and equal argument values must always map to the same key.
Example
calls = []
def add(a, b):
calls.append((a, b))
return a + b
cache = FunctionCache(add, limit=128)
cache.get(1, 2) # -> 3 miss: computed. calls == [(1, 2)]
cache.get(1, 2) # -> 3 hit: same key. calls unchanged
cache.get(2, 3) # -> 5 miss: computed. calls == [(1, 2), (2, 3)]
Eviction should behave like a standard LRU:
def square(n):
return n * n
cache = FunctionCache(square, limit=2)
cache.get(1) # store (LRU -> MRU): 1
cache.get(2) # 1, 2
cache.get(1) # hit; 1 becomes most-recent -> 2, 1
cache.get(3) # miss; over limit, evict LRU (2) -> 1, 3
cache.get(2) # miss again (2 was evicted) -> recomputes
Online Judge
Result will appear here after submission.
Level 2: Add Disk Persistence
Extend the cache so that it survives across process restarts.
- Add a
file_pathparameter to__init__(default"", which means persistence is disabled — behave exactly as in Level 1). - When persistence is enabled, every access is written to disk, and when a new instance is constructed pointing at an existing
file_path, the cache is restored from that file.
Hints
- The cost of a single write must not grow with the cache size. You may not re-serialize and rewrite the entire cache on every access. The intended approach is an append-only log: each access appends one small record.
- After restoring from the log, the cache must reflect the correct state: the most recent value for each key, the correct LRU, then most recent used and then the same capacity/eviction behavior as Level 1.
- Be robust to a truncated final record. A process can crash mid-write. Restoration must tolerate this and recover everything written before it.
Example
# ----- first run -----
c = FunctionCache(square, limit=2, file_path="square.log")
c.get(2) # miss -> 4, record appended to square.log
c.get(3) # miss -> 9, record appended to square.log
# process exits
# ----- second run (fresh process, same file) -----
c = FunctionCache(square, limit=2, file_path="square.log")
c.get(2) # -> 4, restored from disk WITHOUT recomputing
c.get(3) # -> 9, restored from disk WITHOUT recomputing
The restored cache must also preserve recency. If the log's last write for square(2) came after square(3), then after restore the order should be [3, 2], so the next miss evicts 3 first.
Online Judge
Result will appear here after submission.
Level 3 and More Followups
...
Anthropic Crash Course
Fullset Newest Anthropic Coding/System Design Questions, Optimal Solutions and Explanations.
Step_1 Matched with a FAANG+ Senior Engineer
Step_2 Fullset Anthropic Coding/System Design Questions
Step_3 Solutions + step by step Explanation
