Building an LRU cache
A hash map and a doubly linked list, working together for O(1) reads and writes.
The LRU cache is the classic "combine two structures" question, and it is genuinely used everywhere: CPU caches, database buffer pools, CDN edges, HTTP client caches.
Requirement: get and put in O(1), evicting the least recently used entry when full.
Why one structure is not enough
- A hash map gives O(1) lookup but no notion of recency order.
- A doubly linked list maintains order and removes in O(1) — but finding a node in it is O(n).
Combine them: the map stores key → node, and the list stores recency. The map finds the node, the list moves it.
class LRUCache {
#map = new Map();
#head = { key: null, value: null }; // sentinels: most recent side
#tail = { key: null, value: null }; // least recent side
constructor(capacity) {
this.capacity = capacity;
this.#head.next = this.#tail;
this.#tail.prev = this.#head;
}
#unlink(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
#pushFront(node) {
node.next = this.#head.next;
node.prev = this.#head;
this.#head.next.prev = node;
this.#head.next = node;
}
get(key) {
const node = this.#map.get(key);
if (!node) return undefined;
this.#unlink(node);
this.#pushFront(node); // touched: now the most recent
return node.value;
}
put(key, value) {
const existing = this.#map.get(key);
if (existing) {
existing.value = value;
this.#unlink(existing);
this.#pushFront(existing);
return;
}
if (this.#map.size === this.capacity) {
const evict = this.#tail.prev; // the least recently used
this.#unlink(evict);
this.#map.delete(evict.key);
}
const node = { key, value };
this.#pushFront(node);
this.#map.set(key, node);
}
}
The details interviewers check
- Sentinels. With permanent head and tail nodes there are no null checks and no empty-list special case.
- The key on the node. Eviction starts from the list and must delete from the map — without
node.keyyou cannot. getcounts as a use. Forgetting to move the node on read is the most common bug.
The shortcut worth knowing
JavaScript's Map preserves insertion order, so a compact LRU is possible with delete then set to move a key to the end. It is fine for small caches and shows you know the language — but write the linked-list version when the question is about data structures, because that is what is being assessed.