get(key)
and put(key, value)
operations in O(1)
time with automatic eviction of the least recently used item when capacity is exceeded.LRU stands for Least Recently Used. LRU Cache is a type of cache replacement policy that evicts the least recently accessed item when the cache reaches its capacity.
In performance-critical systems (like web servers, databases, or OS memory management), caching helps avoid expensive computations or repeated data fetching. But cache memory is limited so when it's full, we need a policy to decide which item to remove.
LRU chooses the least recently accessed item, based on the assumption that:
“If you haven’t used something for a while, you probably won’t need it soon.”
The LRU strategy is both intuitive and effective. It reflects real-world usage patterns. We tend to access the same small subset of items frequently, and rarely go back to older, untouched entries.
This makes LRU a popular default caching policy in many systems where speed and memory efficiency are critical.
Suppose we have an LRU cache with capacity = 3
, and we perform the following operations:
Final cache state: {3:C, 1:A, 4:D}
(in order of usage from least to most recent)
In this chapter, we will explore the low level design of a LRU cache.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: Should the LRU Cache support generic key-value pairs, or should we restrict it to specific data types?
Interviewer: The cache should be generic and support any type of key-value pair, as long as keys are hashable.
Candidate: Should the cache operations be limited to get
and put
, or do we also need to support deletion?
Interviewer: For now, we can limit operations to get
and put
.
Candidate: What should the get
operation return if the key is not found in the cache?
Interviewer: You can return either null
or a sentinel value like -1
.
Candidate: How should we handle a put
operation on an existing key? Should it be treated as a fresh access and move the key to the most recently used position?
Interviewer: Yes, an update through put
should count as usage. The key should be moved to the front as the most recently used.
Candidate: Will the cache be used in a multi-threaded environment? Do we need to ensure thread safety?
Interviewer: Good question. Assume this cache will be used in a multi-threaded server environment. It must be thread-safe.
Candidate: What are the performance expectations for the get
and put
operations?
Interviewer: Both get
and put
operations must run in O(1)
time on average.
After gathering the details, we can summarize the key system requirements.
get(key)
operation: returns the value if the key exists, otherwise returns null
or -1
put(key, value)
operation: inserts a new key-value pair or updates the value of an existing keyget
and put
operations should update the recency of the accessed or inserted item.<K, V>
), provided the keys are hashable.get
and put
operations must run in O(1) time on average.After the requirements are clear, the next step is to identify the core entities that we will form the foundation of our design.
Unlike systems that model real-world concepts (such as users, products, or bookings), the design of an LRU Cache is centered around choosing the right data structures and internal abstractions to achieve the required functionality and performance specifically, O(1) time complexity for both get
and put
operations.
The core challenge here is twofold:
Let’s break this down.
To efficiently retrieve values by key, we need a data structure that supports constant-time key access.
The natural choice for this is a Hash Map (or Dictionary) since it provides O(1) average-case lookup and update.
The second challenge is maintaining the recency order of cache entries so we can:
An array or a list won’t work, because inserting or moving elements from the middle involves shifting which is an O(N) operation.
Instead, we use a Doubly Linked List since it allows fast removal and insertion of nodes from any position.
Each node maintains references to both its prev
and next
nodes, allowing us to:
The magic of achieving O(1) for both get
and put
lies in combining a hash map and a doubly linked list:
This combination gives us the best of both worlds:
Beyond the HashMap
and Doubly Linked List
, we need two more classes to encapsulate and organize our logic:
Node
: A simple internal class that represents an individual entry in the cache and a node in the linked list.LRUCache
: The main class that exposes the public cache API and coordinates all operations.get
and put
APIs. Manages internal data structures and enforces eviction, insertion, and update rules.These entities form the core abstractions of our LRU Cache. They enable the system to maintain a fixed-size cache, support constant-time access and updates, and evict the least recently used entry when necessary.
Now that we’ve identified the core entities required to build an LRU Cache, the next step is to translate those entities into a well-structured class design.
This includes defining each class's attributes and responsibilities, establishing clear relationships among them, and visualizing the overall architecture through a class diagram.
Our goal is to ensure that the system remains modular, efficient, and easy to reason about all while satisfying the requirement of O(1) time complexity for both get
and put
operations.
We’ll start with the simplest components and build up toward the core coordinating class.
Node<K, V>
Represents an individual entry in the cache, and also serves as a node in the doubly linked list.
key: K
— the unique identifiervalue: V
— the value associated with the keyprev: Node<K, V>
— reference to the previous node in the listnext: Node<K, V>
— reference to the next node in the listStores key-value pairs and enables fast movement and removal in the doubly linked list through its pointers.
DoublyLinkedList<K, V>
A utility class that manages the MRU (Most Recently Used) to LRU (Least Recently Used) ordering of cache entries.
head: Node<K, V>
— dummy head node for easy insertiontail: Node<K, V>
— dummy tail node for easy deletionaddToFront(Node<K, V> node)
— adds a node right after the headremoveNode(Node<K, V> node)
— detaches a node from its current positionmoveToFront(Node<K, V> node)
— removes and re-adds a node to the frontremoveTail(): Node<K, V>
— removes and returns the least recently used node (i.e., the node just before the tail)Maintains the usage order of cache entries and supports O(1) operations for insertion, movement, and removal.
LRUCache<K, V>
The main class that provides the public APIs (get
and put
) and manages the overall cache logic.
capacity: int
— the maximum number of entries allowed in the cachemap: Map<K, Node<K, V>>
— maps keys to their corresponding list nodeslist: DoublyLinkedList<K, V>
— maintains the recency order of nodesget(K key): V
put(K key, V value): void
Coordinates all cache operations, including lookup, insertion, eviction, and recency tracking.
LRUCache
has-a DoublyLinkedList
LRUCache
has-a Map<K, Node<K, V>>
DoublyLinkedList
has-a Node<K, V>
for both head
and tail
LRUCache
uses Node
to store cache entries and manipulate orderingDoublyLinkedList
uses Node
to manage the list structureThis is a generic doubly linked list node that stores:
This enables constant-time addition and removal of nodes from anywhere in the list, a key requirement for efficient LRU eviction.
Implements a doubly linked list with dummy head and tail nodes to simplify edge-case handling.
addFirst(node)
: Adds a node right after the head (most recently used position).remove(node)
: Detaches a node from its current position.moveToFront(node)
: Moves an existing node to the front, marking it as recently used.removeLast()
: Removes and returns the least recently used node (just before the tail).This list manages access order, with the head representing most recently used and the tail representing least recently used.
The LRUCache
class uses two main data structures:
This ensures that both get()
and put()
operations run in O(1) time.
get()
MethodIf the key exists, the corresponding node is moved to the front of the list (most recently used), and its value is returned. If not found, returns null
.
put()
MethodThis ensures LRU behavior while maintaining constant-time operations.
remove()
MethodAllows explicit removal of an entry from the cache, updating both the list and the map.
Thread Safety: The get and put methods are marked as synchronized to ensure that the cache behaves correctly in a multi-threaded environment, preventing race conditions that could corrupt the state of the map and the list.
The demo code shows the LRUCache in action and illustrates the eviction policy.