Thuta Learning
ProjectsProgrammingintermediate

Project: Building an LRU Cache

What you'll walk away with

  • Explain the core ideas behind Project: Building an LRU Cache
  • Run the sample Python code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

A cache needs two things simultaneously: instant lookup by key, and a way to know which entry was used longest ago so it can be evicted when full. A hash map alone gives O(1) lookup but has no notion of order — you'd have to scan every entry to find the least-recently-used one, making eviction O(n). A plain linked list alone gives O(1) reordering (move a node to the front on access) but O(n) lookup, since finding a node by key means walking the list. The LRU cache design beats both naive approaches by combining them: a hash map stores key to node references for instant lookup, and a doubly linked list maintains recency order, with the node's list pointers letting it be unlinked and relinked in O(1) without shifting anything. Every `get` moves the accessed node to the front in O(1); every `put` either updates in place or evicts the tail (least recently used) in O(1) once capacity is exceeded. Python's `OrderedDict` implements exactly this hybrid internally, which is why `move_to_end()` and `popitem(last=False)` are both O(1).

Connect it to a real scenario

This is literally what would sit in front of the Tutorial Platform's lesson-loading logic: hot lessons that many learners request repeatedly (a popular Python intro, a trending JS tutorial) get cached in memory with a fixed capacity, so a repeated request skips re-reading the file or re-hitting the database entirely. When a new lesson is requested and the cache is full, the LRU policy evicts whichever cached lesson hasn't been touched in the longest time — a reasonable bet that it's least likely to be requested again soon, versus evicting arbitrarily or never evicting (which would leak memory as the site's lesson catalog grows past 55 tutorials).

Try the working example

python
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        # OrderedDict internally pairs a hash map with a doubly linked list:
        # move_to_end() and popitem(last=False) are both O(1) because of it.
        self.cache = OrderedDict()

    def get(self, key):
        if key not in self.cache:
            return -1
        # Accessing a key counts as "recently used" -> move it to the front
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            # Refresh existing entry's position before updating
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            # Evict the least-recently-used entry (front of the OrderedDict)
            self.cache.popitem(last=False)

if __name__ == "__main__":
    cache = LRUCache(2)
    cache.put("python-basics", "<lesson html>")
    cache.put("js-intro", "<lesson html>")
    print(cache.get("python-basics"))  # hit -> becomes most recently used
    cache.put("rust-ownership", "<lesson html>")  # evicts "js-intro"
    print(cache.get("js-intro"))  # -1, was evicted
    print(list(cache.cache.keys()))  # remaining cached lesson slugs
You should see
Prints `<lesson html>`, then `-1`, then the remaining cache keys `['python-basics', 'rust-ownership']`, showing that `js-intro` was evicted.

5-minute try-it

Add a `stats()` method that tracks the cache's hit rate — total get calls, hit count, and miss count.

One important caution

Forgetting to call `move_to_end()` on a `get` — reads then don't refresh recency order, silently breaking LRU semantics so you evict entries that were actually just used

Checking capacity with the wrong comparison or at the wrong point in `put` (e.g. before inserting the new key) causes an off-by-one that either evicts one entry too early or lets the cache grow past its stated capacity

Python Docs — functools.lru_cacheData Structures & Algorithms

Easy traps

  • Forgetting to call `move_to_end()` on a `get` — reads then don't refresh recency order, silently breaking LRU semantics so you evict entries that were actually just used
  • Checking capacity with the wrong comparison or at the wrong point in `put` (e.g. before inserting the new key) causes an off-by-one that either evicts one entry too early or lets the cache grow past its stated capacity
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Add a `stats()` method that tracks the cache's hit rate — total get calls, hit count, and miss count.

You'll know it worked when: Prints `<lesson html>`, then `-1`, then the remaining cache keys `['python-basics', 'rust-ownership']`, showing that `js-intro` was evicted.

Project: Building an LRU Cache | Thuta Learning