LRU Cache: Doubly-Linked List & Hash Map Playground
Interactive visualization of how an LRU Cache achieves guaranteed $O(1)$ key lookup and $O(1)$ node promotion. Click keys below to trigger cache reads and watch node pointers splice in real time.
Most Recent
Eviction Target
Cache initialized with 4 warm nodes. Click any key above to inspect pointer updates.
Machine Coding: Thread-Safe LRU Cache with TTL Eviction
Implementing an In-Memory Cache with Least Recently Used (LRU) eviction and Time-To-Live (TTL) expiration is one of the most tested problems in top-tier machine coding rounds.
You must achieve:
- $O(1)$ Time Complexity for both
get(key)andput(key, value). - $O(1)$ Eviction of the least recently used element when capacity is reached.
- Expiration Support: Entries automatically expire after a specified duration.
- Thread Safety: Concurrent access across multiple reader and writer threads without data corruption.
1. The Core Architecture: Hash Map + Doubly Linked List
Why are both data structures required?
- A standard Hash Map gives $O(1)$ key-value lookup, but has no inherent ordering to track recency.
- An Array or Singly Linked List maintains order, but finding a node to update its position takes $O(N)$ linear scan time.
- A Doubly Linked List paired with a Hash Map storing pointers directly to the list nodes enables instant $O(1)$ removal and promotion to head!
┌───────────────────────────────────────────────┐
│ ConcurrentHashMap<K, Node<K,V>> │
│ "A" ──► Node A │
│ "B" ──► Node B │
│ "C" ──► Node C │
└───────────────────────┬───────────────────────┘
│ Direct Pointers
▼
[HEAD Sentinel] ◄──► [Node B (Newest)] ◄──► [Node A] ◄──► [Node C (Oldest)] ◄──► [TAIL Sentinel]
Node Data Structure
class Node<K, V> {
K key;
V value;
long expiryTimestamp; // Epoch millisecond
Node<K, V> prev;
Node<K, V> next;
public Node(K key, V value, long ttlMillis) {
this.key = key;
this.value = value;
this.expiryTimestamp = ttlMillis > 0 ? System.currentTimeMillis() + ttlMillis : Long.MAX_VALUE;
}
public boolean isExpired() {
return System.currentTimeMillis() > expiryTimestamp;
}
}
2. $O(1)$ Operational Flow
get(key):
- Lookup
nodein Hash Map. If missing, returnnull. - Check
node.isExpired(). If expired, remove node from map and list, and returnnull. - Detach
nodefrom its current position in the linked list. - Insert
nodedirectly afterHEAD(promoted to most recently used). - Return
node.value.
put(key, value, ttl):
- If
keyalready exists, update value, refresh TTL, and promote node toHEAD. - If
keyis new:- If
cache.size() >= capacity: Remove the node immediately precedingTAIL(the least recently used item) from both the linked list and the Hash Map ($O(1)$ eviction). - Create new
Node, link it immediately afterHEAD, and insert into Hash Map.
- If
3. TTL Expiration Strategies: Passive vs Active Cleanup
How do we purge expired keys without blocking active requests?
1. Passive Eviction (On-Access)
Checked synchronously whenever get(key) or containsKey(key) is invoked. If the timestamp has passed, the item is deleted on the spot.
- Limitation: If a key is written with a 5-second TTL and never requested again, it lingers in memory indefinitely.
2. Active Eviction (Background Sweeper)
Run a single daemon thread with a scheduled executor (e.g. running every 1 second):
- Redis Sampling Strategy: Instead of scanning 1,000,000 keys (which would cause a massive CPU freeze), sample 20 random keys with TTLs. Evict all expired ones. If $>25%$ of the sampled keys were expired, repeat the sweep immediately!
Interactive LRU Visualizer
Use the LRU Cache Playground above to step through operations:
- Insert keys (
A,B,C,D) and watch the doubly linked list pointers update dynamically. - Read an older key to watch it promoted to
HEADin $O(1)$ time. - Exceed capacity to observe the exact node evicted from
TAIL.