LRU Cache
The famous DSA problem and the LLD version of it ask different questions. The DSA version
wants an algorithm that hits O(1) per operation. The LLD version wants that algorithm
wrapped in a class whose public surface never leaks the doubly linked list it's built on -
callers only ever see keys and values.
Requirements
Functional
get(key)returns the value forkey, or a not-found signal if it isn't cached, and counts as a "use" of that key.put(key, value)inserts or updates an entry and counts as a "use" of that key. If the cache is at capacity and a new key needs to be inserted, the least-recently-used entry is evicted first.
Non-functional
- Both operations run in
O(1)time regardless of how many entries the cache holds. - The eviction bookkeeping is an internal detail - nothing about
LRUCache's public methods should hint that a linked list is involved.
Design
LRUCache holds two collaborators that solve the two halves of the problem separately: a
hash map for O(1) lookup by key, and a doubly linked list ordered by recency for O(1)
reordering and eviction. Neither one alone gets you both properties - a map alone can't
tell you what's least recently used without a scan, and a list alone can't find a key
without one.
- 1The client only ever sees keys and values - never a node or a pointer.
- 2The map answers in O(1) whether the key exists and, if so, which node holds it.
- 3A hit promotes that node to most-recently-used by splicing it to the front.
- 4Same entry point handles both a fresh key and an update to an existing one.
- 5Only triggered if the cache is full and the key is new - the tail node is removed first.
- 6The evicted node is dropped from the map too, or a stale entry would sit there forever.
The map stores key -> Node, never key -> value directly, because the node is also what
lets the cache splice an entry out of the middle of the list in O(1) the moment it's
touched again.
Class diagram
Code
Design decisions
- A map plus a doubly linked list, not a map with timestamps. Timestamps would need a
scan over every entry to find the oldest one -
O(n)eviction. A list ordered by recency turns "find the least-recently-used entry" into "look at the tail," which isO(1)no matter how large the cache gets. - Doubly linked, not singly linked. Moving a middle node to the front on every
getrequires detaching it from its current neighbors in one step. A singly linked list would need to walk from the head to find a node's predecessor first, which reintroduces theO(n)cost this design exists to avoid. - Sentinel head and tail nodes instead of nullable pointers. Every insert and remove
path becomes a two-line splice with no
if (head == null)special case for an empty list or a single-element list - the sentinels guarantee there is always a real node on both sides of any real node. Nodeis never returned from a public method.getandputonly ever hand back or accept plain values - the fact that eviction order is implemented as a linked list is free to change later (a skip list, a heap keyed by last-used time) without touching a single caller.- What's missing for a real system: this design isn't thread-safe - concurrent
get/putcalls need a lock around the map-and-list pair, or a striped-lock scheme to avoid one global bottleneck. A production cache would likely also want a per-entry TTL and size-based (not just count-based) eviction, neither of which this page's scope calls for.