Design an LRU Cache

The canonical LLD coding question: O(1) get and put with a hash map + doubly linked list. Full implementation, the why, and the follow-ups (thread-safety, TTL, LFU).

LLDdata-structurescache

The ask

A fixed-capacity cache with O(1) get(key) and put(key, value); when full, evict the least-recently-used entry.

The insight

You need two things in O(1): lookup by key and "move this to most-recent" / "find the least-recent". No single structure gives both, so combine:

  • a hash map key → node for O(1) lookup, and
  • a doubly linked list in recency order (head = most recent, tail = least), so moving a node or evicting the tail is O(1).

See it run

Step through the canonical trace below. Watch how get promotes a key to the front, and how a put into a full cache evicts the tail (the least-recently-used key). The map and the list highlight together — that pairing is the whole trick. Edit the capacity or the operations to test your own sequence.

LRU cache — map + doubly linked listtime O(1) get/putspace O(capacity)
HashMap · key → node
(empty)
Doubly linked list · MRU ↔ LRU
headMRU
tailLRU

size 0/2 · left = most recent, right = first to be evicted

1/15Empty cache, capacity 2. The map (top) finds any key in O(1). The list (bottom) keeps everything in recency order — most-recently-used on the left, least-recently-used on the right.

size = 0/2

Think it through like the interview

Don't memorize the structure — derive it. The derivation is reusable; the memorized answer dies on the first follow-up.

Think it through: LRU CacheLLD Coding Classic — LeetCode 1460/5 stages

PROBLEMBuild a fixed-capacity cache with O(1) get(key) and put(key, value). When full, evict the least-recently-used entry.

  1. 1

    Turn 'LRU' into operations

    Forget data structures. What operations must be O(1), stated precisely?

  2. 2

    Audit the candidates

    Which single structure gives all three in O(1)? Try each and find its failure.

    unlocks after the stage above
  3. 3

    Combine: each covers the other's weakness

    Map gives lookup but no order; doubly linked list gives O(1) reorder but no lookup. How do I wire them together?

    unlocks after the stage above
  4. 4

    Kill the edge cases structurally

    Empty list, single element, removing the head… how do I avoid a forest of null checks?

    unlocks after the stage above
  5. 5

    Trace it, then take the follow-ups

    capacity=2: put(1), put(2), get(1), put(3). Who got evicted — and what breaks under threads?

    unlocks after the stage above

Implementation

class Node {
  constructor(
    public key: number,
    public val: number,
    public prev: Node | null = null,
    public next: Node | null = null,
  ) {}
}

class LRUCache {
  private map = new Map<number, Node>();
  private head = new Node(0, 0); // sentinel MRU side
  private tail = new Node(0, 0); // sentinel LRU side

  constructor(private capacity: number) {
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  private remove(n: Node) {
    n.prev!.next = n.next;
    n.next!.prev = n.prev;
  }
  private addFront(n: Node) {
    n.next = this.head.next;
    n.prev = this.head;
    this.head.next!.prev = n;
    this.head.next = n;
  }

  get(key: number): number {
    const n = this.map.get(key);
    if (!n) return -1;
    this.remove(n);
    this.addFront(n); // mark most-recently-used
    return n.val;
  }

  put(key: number, val: number): void {
    const existing = this.map.get(key);
    if (existing) { existing.val = val; this.remove(existing); this.addFront(existing); return; }
    if (this.map.size === this.capacity) {
      const lru = this.tail.prev!;       // evict least-recently-used
      this.remove(lru);
      this.map.delete(lru.key);
    }
    const n = new Node(key, val);
    this.addFront(n);
    this.map.set(key, n);
  }
}
Sentinel nodes remove the edge cases

The dummy head/tail nodes mean remove/addFront never check for null neighbors — no special-casing the empty list or single element. Mention this; it shows you write clean pointer code.

Complexity & follow-ups

  • Time: O(1) for both get and put. Space: O(capacity).
  • Thread-safety: wrap operations in a lock, or use a concurrent structure; note the lock is the contention point at scale (shard the cache to reduce it).
  • TTL: store an expiry per node and treat expired as a miss (lazy) + a sweeper.
  • LFU instead of LRU: evict least-frequently-used — needs frequency buckets; more complex, better for skewed access.

Practice — level up

The whole skill here is pairing a hash map with a second structure to make every operation O(1). Climb this ladder and that move becomes automatic — every rung is the same idea under a new disguise.

Practice ladder: O(1) cache design0/6 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up — build the two halves

Implement each structure alone before you combine them.
  1. Buckets + chaining — the lookup half of the cache.

  2. Pointer splicing with a sentinel — the recency half.

Core — the cache itself

Map → node, move-to-front, evict the tail.
  1. LRU CacheMedium

    The canonical map + doubly linked list in O(1).

  2. Same trick: map (key → index) paired with an array.

Stretch — harder eviction policies

Say out loud which structure each new requirement forces.
  1. Add frequency buckets — a map of maps, evict the least-frequent.

  2. Buckets of equal-count keys in a linked list — increment/decrement in O(1).