Skip to content
intermediatePhase 46 · Caching

LRU Cache

Implement Least Recently Used eviction for memory-efficient caching.

45m
0 problems
Topic Progress0%

LRU Algorithm

LRU Algorithm Deep Dive

LRU (Least Recently Used) is an eviction algorithm that removes the item that hasn't been accessed for the longest time.

Core Concept

Principle: Recently accessed items are more likely to be accessed again.

Access Pattern: A B C D A B E

State Tracking:
[A] → [B,A] → [C,B,A] → [D,C,B,A] → [A,D,C,B] → [B,A,D,C] → [E,B,A,D]
      MRU          MRU          MRU          MRU          MRU          MRU
      LRU          LRU          LRU          LRU          LRU          LRU

When capacity exceeded, evict LRU item (leftmost)

Operations

Operation Description Time Complexity
GET(key) Access item, move to MRU O(1)
PUT(key, val) Insert/update, evict LRU if needed O(1)
DELETE(key) Remove specific item O(1)

Why O(1) is Possible

  • HashMap: O(1) key lookup
  • Doubly Linked List: O(1) reordering
  • Combined: O(1) for all operations

Implementation with HashMap+LinkedList

Implementation with HashMap + LinkedList

Complete O(1) LRU Cache

class Node:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = {}  # key -> Node
        
        # Dummy head and tail for easier manipulation
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head
    
    def _remove(self, node):
        """Remove node from linked list"""
        node.prev.next = node.next
        node.next.prev = node.prev
    
    def _add_to_front(self, node):
        """Add node right after head (most recently used)"""
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node
    
    def get(self, key):
        if key in self.cache:
            node = self.cache[key]
            # Move to front (most recently used)
            self._remove(node)
            self._add_to_front(node)
            return node.value
        return -1
    
    def put(self, key, value):
        if key in self.cache:
            # Update existing
            node = self.cache[key]
            node.value = value
            self._remove(node)
            self._add_to_front(node)
        else:
            # Add new
            if len(self.cache) >= self.capacity:
                # Evict LRU (node before tail)
                lru = self.tail.prev
                self._remove(lru)
                del self.cache[lru.key]
            
            new_node = Node(key, value)
            self.cache[key] = new_node
            self._add_to_front(new_node)

# Usage
lru = LRUCache(2)
lru.put(1, 1)    # Cache: {1:1}
lru.put(2, 2)    # Cache: {2:2, 1:1}
lru.get(1)       # Returns 1, Cache: {1:1, 2:2}
lru.put(3, 3)    # Evicts 2, Cache: {3:3, 1:1}
lru.get(2)       # Returns -1 (evicted)

Python OrderedDict Shortcut

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cache = OrderedDict()
        self.capacity = capacity
    
    def get(self, key):
        if key in self.cache:
            self.cache.move_to_end(key)
            return self.cache[key]
        return -1
    
    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)

Memory Layout

Doubly Linked List with Dummy Nodes:

[Head] ⇄ [A] ⇄ [B] ⇄ [C] ⇄ [Tail]
          ↑                ↑
         MRU              LRU

When accessing B:
[Head] ⇄ [B] ⇄ [A] ⇄ [C] ⇄ [Tail]
          ↑              ↑
         MRU            LRU

Use Cases

LRU Cache Use Cases

1. Database Query Cache

class QueryCache:
    def __init__(self, capacity=10000):
        self.cache = LRUCache(capacity)
    
    def execute_query(self, query, params):
        cache_key = hash(query + str(params))
        
        result = self.cache.get(cache_key)
        if result is not None:
            return result  # Cache hit
        
        # Cache miss - execute query
        result = db.execute(query, params)
        self.cache.put(cache_key, result)
        return result

2. DNS Cache

class DNSCache:
    def __init__(self, capacity=1000):
        self.cache = LRUCache(capacity)
    
    def resolve(self, hostname):
        cached = self.cache.get(hostname)
        if cached:
            return cached
        
        # Resolve and cache
        ip = dns.resolve(hostname)
        self.cache.put(hostname, {
            'ip': ip,
            'timestamp': time.time()
        })
        return ip

3. Web Browser Cache

Browser LRU Cache:

Page Load: A → B → C → D → A → B → E

Cache State (capacity 4):
[A,B,C,D] → [A,B,C,D] → [A,B,C,D] → [B,C,D,A] → [C,D,A,B] → [D,A,B,E]
                                              ↑ Evict C     ↑ Evict D

4. CDN Edge Cache

class CDNEdgeCache:
    def __init__(self, max_size_gb=100):
        self.cache = LRUCache(capacity=1000000)
        self.size_tracker = SizeTracker(max_size_gb)
    
    def get_content(self, url):
        content = self.cache.get(url)
        if content:
            return content
        
        # Fetch from origin
        content = fetch_from_origin(url)
        
        if self.size_tracker.can_fit(content.size):
            self.cache.put(url, content)
        
        return content

5. Application-Level Cache

# Product catalog cache
product_cache = LRUCache(capacity=50000)

def get_product(product_id):
    product = product_cache.get(product_id)
    if product:
        return product
    
    product = db.products.find(product_id)
    product_cache.put(product_id, product)
    return product

Practice Problems

0/3solved
Design LRU Cache System

Design a scalable LRU Cache system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
LRU Cache Scaling

How would you scale LRU Cache to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
LRU Cache Failure Modes

Analyze potential failure modes for LRU Cache and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. What data structures are combined to achieve O(1) LRU?

Question 1 options

2. In LRU, which item is evicted when capacity is reached?

Question 2 options

3. What happens when you GET a key in LRU?

Question 3 options

4. Why use dummy head and tail nodes in LRU implementation?

Question 4 options

5. Which real-world system commonly uses LRU caching?

Question 5 options

Flashcards

Question

How to implement LRU in O(1)?

Answer

HashMap for O(1) key lookup + Doubly Linked List for O(1) reordering. HashMap stores key->Node, Linked List maintains access order.

Question

What is the purpose of dummy head/tail in LRU?

Answer

Eliminates edge cases when inserting at beginning or deleting from end of the linked list, simplifying implementation.

Question

What happens on GET in LRU?

Answer

Node is removed from current position and added to front (MRU position). Returns value if found, -1 otherwise.

Question

Name 3 real-world LRU use cases

Answer

1) Database query cache, 2) DNS cache, 3) Web browser page cache, 4) CDN edge cache

Question

What is the time complexity of LRU operations?

Answer

O(1) for GET, PUT, and DELETE operations - all operations are constant time with HashMap + Linked List.

Revision Notes

Key Takeaways

  • 1.LRU achieves O(1) using HashMap + Doubly Linked List
  • 2.GET moves item to front (MRU), evict from back (LRU) on overflow
  • 3.Dummy head/tail nodes simplify edge cases
  • 4.Widely used: DNS, browsers, databases, CDNs
  • 5.Python's OrderedDict provides built-in LRU functionality

Interview Tips

  • Be able to implement LRU from scratch in O(1)
  • Explain why both HashMap and Linked List are needed
  • Discuss memory overhead of maintaining the linked list
  • Mention OrderedDict as a shortcut but understand the underlying implementation

Cheat Sheet

Cheat Sheet: LRU Cache

Data Structures

  • HashMap: O(1) key lookup
  • Doubly Linked List: O(1) reordering

Operations

  • GET: Remove from current position, add to front (MRU)
  • PUT: Add to front, evict from back (LRU) if at capacity

Implementation

# With dummy nodes
class Node:
    def __init__(self, key=0, val=0):
        self.key, self.val = key, val
        self.prev = self.next = None

# O(1) using OrderedDict
from collections import OrderedDict

Use Cases

  • Database query cache
  • DNS cache
  • Browser page cache
  • CDN edge cache