LRU Cache (Least Recently Used Cache)

02 Aug 2026, Updated: 03 Aug 2026 6 min read
2
An LRU (Least Recently Used) Cache is a fixed-size cache that automatically removes the least recently accessed entry when the cache reaches its capacity.

Every read or write operation marks an entry as recently used. The primary requirement is that both get() and put() operations execute in O(1) time.

Requirements

A correct LRU cache implementation should meet the following requirements:

1. The cache stores key-value pairs.
2. When a key is accessed using get(), it becomes the most recently used item.
3. When a key is inserted or updated using put(), it also becomes the most recently used item.
4. If the cache is full, inserting a new item removes the least recently used item.
5. Both get() and put() operations execute in O(1) time.

Design

A HashMap provides O(1) lookup by key.

A Doubly Linked List maintains the usage order. Each node stores references to both its previous and next nodes, allowing nodes to be removed or inserted in O(1) time.

Since the HashMap already holds a direct reference to the corresponding node, no traversal of the linked list is required.

The head always represents the most recently used node. The tail always represents the least recently used node.

Every cache entry is represented by a node stored both in the HashMap and in the Doubly Linked List.

Operations

Both operations take O(1) time because the HashMap provides constant-time lookup, while the doubly linked list allows constant-time insertion, removal, and movement of nodes without traversing the list.

get(key)

The key is searched in the HashMap. If it is not found, return null. If found, move the node to the front of the linked list and return its value.

put(key, value)

If the key already exists, update its value, move it to the front, and finish. If the key does not exist and the cache has free space, create a new node, insert it at the front, and add it to the map.

If the cache is full, remove the node at the tail, delete it from the map, create a new node, insert it at the front, and add it to the map.

Java Implementation

Whenever get() or put() accesses an existing entry, it is moved to the front of the list.

When the cache reaches its capacity, the last node in the list is removed and its corresponding entry is deleted from the HashMap. This ensures that all cache operations execute in O(1) time.
import java.util.HashMap;
import java.util.Map;

public class LRUCache<K, V> {

    private final int capacity;
    private final Map<K, Node<K, V>> cache = new HashMap<>();

    private final Node<K, V> head;
    private final Node<K, V> tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;

        head = new Node<>(null, null);
        tail = new Node<>(null, null);

        head.next = tail;
        tail.prev = head;
    }

    public V get(K key) {
        Node<K, V> node = cache.get(key);

        if (node == null) {
            return null;
        }

        moveToFront(node);
        return node.value;
    }

    public void put(K key, V value) {

        Node<K, V> node = cache.get(key);

        if (node != null) {
            node.value = value;
            moveToFront(node);
            return;
        }

        if (cache.size() == capacity) {
            Node<K, V> lru = removeLast();
            cache.remove(lru.key);
        }

        Node<K, V> newNode = new Node<>(key, value);

        addFirst(newNode);
        cache.put(key, newNode);
    }

    private void moveToFront(Node<K, V> node) {
        remove(node);
        addFirst(node);
    }

    private void addFirst(Node<K, V> node) {
        node.next = head.next;
        node.prev = head;

        head.next.prev = node;
        head.next = node;
    }

    private void remove(Node<K, V> node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private Node<K, V> removeLast() {
        Node<K, V> node = tail.prev;
        remove(node);
        return node;
    }

    private static class Node<K, V> {

        K key;
        V value;

        Node<K, V> prev;
        Node<K, V> next;

        Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
}
This implementation is not thread-safe because both the HashMap and the doubly linked list are modified by every cache operation.

Example

This example demonstrates how the least recently used entry is automatically evicted when the cache reaches its maximum capacity.
public class Main {
    public static void main(String[] args) {

        LRUCache<Integer, String> cache = new LRUCache<>(3);

        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");

        cache.get(1);
        cache.put(4, "D");

        System.out.println(cache.get(2)); // null
        System.out.println(cache.get(1)); // A
        System.out.println(cache.get(3)); // C
        System.out.println(cache.get(4)); // D
    }
}

Execution

Initially (Empty Cache):
Head β†’ Tail

After put(1), put(2), and put(3):
Head β†’ 3 β†’ 2 β†’ 1 β†’ Tail

After get(1):
Head β†’ 1 β†’ 3 β†’ 2 β†’ Tail

After put(4):
Least Recently Used (key 2) is evicted.

Head β†’ 4 β†’ 1 β†’ 3 β†’ Tail

Complexity

get() performs a HashMap lookup and updates the linked list in constant time. put() performs insertion, update, eviction, and linked list operations in constant time.

Time Complexity
get() β†’ O(1)
put() β†’ O(1)

Space Complexity
O(capacity)

LRU Cache Using LinkedHashMap

We can also use LinkedHashMap, which already maintains insertion or access order and can implement an LRU cache by overriding removeEldestEntry().
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // accessOrder = true
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }

    public static void main(String[] args) {

        LRUCache<Integer, String> cache = new LRUCache<>(3);

        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");

        cache.get(1);      // 1 becomes most recently used
        cache.put(4, "D"); // Removes key 2

        System.out.println(cache); // {3=C, 1=A, 4=D}
    }
}

Making the LRU Cache Thread-Safe

The simplest approach is to protect all cache operations with a ReentrantLock.
import java.util.concurrent.locks.ReentrantLock;

public class LRUCache<K, V> {
    private final ReentrantLock lock = new ReentrantLock();

    // Existing fields...

    public V get(K key) {
        lock.lock();
        try {
            Node<K, V> node = cache.get(key);
            if (node == null) {
                return null;
            }
            moveToFront(node);
            return node.value;
        } finally {
            lock.unlock();
        }
    }

    public void put(K key, V value) {
        lock.lock();
        try {
            Node<K, V> node = cache.get(key);
            if (node != null) {
                node.value = value;
                moveToFront(node);
                return;
            }
            if (cache.size() == capacity) {
                Node<K, V> lru = removeLast();
                cache.remove(lru.key);
            }
            Node<K, V> newNode = new Node<>(key, value);
            addFirst(newNode);
            cache.put(key, newNode);
        } finally {
            lock.unlock();
        }
    }
    // Other methods remain unchanged...
}
Alternatively, the public methods can simply be synchronized.
public synchronized V get(K key) {
    // Existing implementation
}

public synchronized void put(K key, V value) {
    // Existing implementation
}
Using ReentrantLock is generally preferred because it provides more flexibility, such as tryLock(), timed lock acquisition, and interruptible locking.

Conclusion

The LRU Cache is a classic Low-Level Design problem that combines HashMap and Doubly Linked List to achieve efficient cache operations with optimal time complexity.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer β€’ Java β€’ Python β€’ Distributed Systems β€’ AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

πŸ’¬ Comments

Join the Discussion