Debug a Linked-List LRU Cache That Evicts the Wrong Keys and Crashes

Read the full interview experience this question came from →

Quick Overview

A debugging exercise built around a least-recently-used cache implemented with a dictionary and a doubly linked list that evicts the wrong keys and eventually crashes. It tests tracing pointer updates, finding every defect rather than the first, keeping data structures consistent, and writing invariant and reference-model tests.

Debug a Linked-List LRU Cache That Evicts the Wrong Keys and Crashes

Company: Google

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Onsite

You are handed a fixed-capacity least-recently-used (LRU) cache and asked to debug it. The intended behavior: - `LRUCache(capacity)` creates a cache that holds at most `capacity` keys. - `get(key)` returns the stored value, or `-1` if the key is absent. A successful `get` counts as a use of that key. - `put(key, value)` inserts or updates a key; an update also counts as a use. Inserting a new key into a full cache first evicts the least recently used key. - Both operations should run in constant average time. The code and language used in the interview are not reported. The Python version below is a stand-in written for practice: it uses a dictionary plus a doubly linked list with sentinel nodes, and it contains several defects. ```python class Node: def __init__(self, key, value): self.key = key self.value = value self.prev = None self.next = None class LRUCache: def __init__(self, capacity): self.capacity = capacity self.nodes = {} # key -> Node self.head = Node(None, None) # sentinel before the most recently used entry self.tail = Node(None, None) # sentinel after the least recently used entry self.head.next = self.tail self.tail.prev = self.head def _unlink(self, node): node.prev.next = node.next node.next.prev = node.prev def _push_front(self, node): node.prev = self.head node.next = self.head.next self.head.next = node def get(self, key): node = self.nodes.get(key) if node is None: return -1 return node.value def put(self, key, value): if key in self.nodes: node = self.nodes[key] node.value = value self._unlink(node) self._push_front(node) return if len(self.nodes) > self.capacity: lru = self.tail.prev self._unlink(lru) node = Node(key, value) self.nodes[key] = node self._push_front(node) ``` This test fails: ```python cache = LRUCache(2) cache.put(1, 1) cache.put(2, 2) assert cache.get(1) == 1 cache.put(3, 3) # should evict key 2 assert cache.get(2) == -1 cache.put(4, 4) # should evict key 1 assert cache.get(1) == -1 assert cache.get(3) == 3 assert cache.get(4) == 4 ``` ### Constraints and Clarifications - Keys and values are non-negative integers. - Assume single-threaded use unless you ask otherwise. - Keep the dictionary-plus-linked-list design: the goal is to fix it, not to replace it with a library class. ### Clarifying Questions - What should a cache with capacity 0 do on `put`? - Is the cache ever shared between threads? - Can `-1` ever be a legitimate stored value? ### Part 1 — Find the defects Trace the failing test through the code, explaining exactly what happens at each step, then list every defect, including those the test has not reached yet. For each one, describe the symptom a caller would see. ```hint Check both directions For every pointer a helper changes, ask which pointer on the neighboring node should have changed with it. ``` #### What This Part Should Cover - A step-by-step trace that explains the first wrong result and what happens after it - Defects in the list wiring, the recency update, the eviction condition and the dictionary bookkeeping - How some defects mask others until the first one is fixed ### Part 2 — Fix it and prove it Write the corrected class, then describe the checks and tests that would have caught these defects early. ```hint Make corruption loud Think of a check you could run after every operation that fails the moment the list and the dictionary disagree. ``` #### What This Part Should Cover - Corrected code that keeps constant-time operations - An explicit decision for capacity 0 - An invariant check and a test strategy that goes beyond the single sample test ### What a Strong Answer Covers - Systematic debugging: reproduce, trace, form a hypothesis, verify, rather than guessing - Finds every defect, not only the first failing assertion - Understands why the sentinel nodes exist and how a missing back pointer corrupts eviction - Keeps the dictionary and the list consistent on every code path - Adds tests that check invariants and compare against a simple reference model ### Follow-up Questions - How would you make this cache safe for concurrent use, and what does locking cost on a hot path? - How would you add a per-entry time-to-live without losing constant-time operations? - Why might a production cache prefer an approximate eviction policy over exact LRU?

Overview: A debugging exercise built around a least-recently-used cache implemented with a dictionary and a doubly linked list that evicts the wrong keys and eventually crashes. It tests tracing pointer updates, finding every defect rather than the first, keeping data structures consistent, and writing invariant and reference-model tests.

Read the full Google Software Engineer interview experience this question came from

Google logo
Google
Aug 15, 2026
mediumSoftware EngineerOnsiteSoftware Engineering Fundamentals
0
0

You are handed a fixed-capacity least-recently-used (LRU) cache and asked to debug it. The intended behavior:

  • LRUCache(capacity) creates a cache that holds at most capacity keys.
  • get(key) returns the stored value, or -1 if the key is absent. A successful get counts as a use of that key.
  • put(key, value) inserts or updates a key; an update also counts as a use. Inserting a new key into a full cache first evicts the least recently used key.
  • Both operations should run in constant average time.

The code and language used in the interview are not reported. The Python version below is a stand-in written for practice: it uses a dictionary plus a doubly linked list with sentinel nodes, and it contains several defects.

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


class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.nodes = {}               # key -> Node
        self.head = Node(None, None)  # sentinel before the most recently used entry
        self.tail = Node(None, None)  # sentinel after the least recently used entry
        self.head.next = self.tail
        self.tail.prev = self.head

    def _unlink(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _push_front(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next = node

    def get(self, key):
        node = self.nodes.get(key)
        if node is None:
            return -1
        return node.value

    def put(self, key, value):
        if key in self.nodes:
            node = self.nodes[key]
            node.value = value
            self._unlink(node)
            self._push_front(node)
            return
        if len(self.nodes) > self.capacity:
            lru = self.tail.prev
            self._unlink(lru)
        node = Node(key, value)
        self.nodes[key] = node
        self._push_front(node)

This test fails:

cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
assert cache.get(1) == 1
cache.put(3, 3)            # should evict key 2
assert cache.get(2) == -1
cache.put(4, 4)            # should evict key 1
assert cache.get(1) == -1
assert cache.get(3) == 3
assert cache.get(4) == 4

Constraints and Clarifications

  • Keys and values are non-negative integers.
  • Assume single-threaded use unless you ask otherwise.
  • Keep the dictionary-plus-linked-list design: the goal is to fix it, not to replace it with a library class.

Clarifying Questions Guidance

  • What should a cache with capacity 0 do on put ?
  • Is the cache ever shared between threads?
  • Can -1 ever be a legitimate stored value?

Part 1 — Find the defects

Trace the failing test through the code, explaining exactly what happens at each step, then list every defect, including those the test has not reached yet. For each one, describe the symptom a caller would see.

What This Part Should Cover Guidance

  • A step-by-step trace that explains the first wrong result and what happens after it
  • Defects in the list wiring, the recency update, the eviction condition and the dictionary bookkeeping
  • How some defects mask others until the first one is fixed

Part 2 — Fix it and prove it

Write the corrected class, then describe the checks and tests that would have caught these defects early.

What This Part Should Cover Guidance

  • Corrected code that keeps constant-time operations
  • An explicit decision for capacity 0
  • An invariant check and a test strategy that goes beyond the single sample test

What a Strong Answer Covers Guidance

  • Systematic debugging: reproduce, trace, form a hypothesis, verify, rather than guessing
  • Finds every defect, not only the first failing assertion
  • Understands why the sentinel nodes exist and how a missing back pointer corrupts eviction
  • Keeps the dictionary and the list consistent on every code path
  • Adds tests that check invariants and compare against a simple reference model

Follow-up Questions Guidance

  • How would you make this cache safe for concurrent use, and what does locking cost on a hot path?
  • How would you add a per-entry time-to-live without losing constant-time operations?
  • Why might a production cache prefer an approximate eviction policy over exact LRU?