REUSABLE COMPONENT DESIGN / OBJECT DESIGN BRIEF

Cache design

Designing an in-memory cache is a classic machine-coding question: implement get(key) and put(key, value) in O(1) with a capacity limit and an eviction policy, usually LRU (least recently used).

IntermediatePhase 08 / Topic 1 of 7ResponsibilitiesCollaborationsExtensibility
01

Overview

Designing an in-memory cache is a classic machine-coding question: implement get(key) and put(key, value) in O(1) with a capacity limit and an eviction policy, usually LRU (least recently used). The standard solution combines a hash map (key to node) with a doubly linked list ordered by recency; get moves a node to the front, put inserts at the front and evicts from the tail when full.

A strong LLD answer goes further: pluggable eviction policies (LRU, LFU, FIFO) via Strategy, TTL expiry, thread safety, statistics (hit rate), and a clean generic interface Cache<K, V>. These extensions show design skill beyond the data structure itself.

A desk with limited space

You keep the books you used most recently on your desk. When the desk is full and you need a new one, you put back the book you have not touched for the longest time.

02

When to use it

  • Interview prompts: 'Design an LRU cache' or 'Design a cache with eviction policies'.
  • Memoizing expensive computations or remote calls.
  • Bounded memory with hot data reused often.
03

Where it shows up in interviews

O(1) LRU

Recognize it when: get and put in constant time with capacity.

  • LRU Cache (LeetCode 146)
  • LFU Cache (LeetCode 460)
Extensible cache

Recognize it when: multiple eviction policies, TTL, thread safety.

  • Design a generic cache library
  • Design a cache layer for a service
04

Where it is used in real software

Caffeine and Guava caches

Java libraries with size limits, TTLs, stats, and near-optimal eviction (W-TinyLFU).

java.util.LinkedHashMap

accessOrder=true plus removeEldestEntry gives a simple LRU cache.

Redis eviction policies

allkeys-lru, allkeys-lfu, volatile-ttl chosen by configuration.

05

Key terms

Eviction policy
Rule choosing which entry to remove when full.
LRU / LFU / FIFO
Least recently used / least frequently used / first in, first out.
TTL
Entry expires after a time.
Hit rate
Fraction of gets served from the cache.
Doubly linked list
O(1) move and remove when you hold the node.
06

How it works, step by step

  1. 1
    Define the interface

    get(key), put(key, value), size(), optional ttl.

  2. 2
    Map for O(1) lookup

    key -> node.

  3. 3
    Linked list for recency

    Head = most recent, tail = eviction candidate.

  4. 4
    Extract eviction as a strategy

    EvictionPolicy with keyAccessed and evict.

  5. 5
    Add thread safety and TTL

    Lock or striped locks; expiry on read and background cleanup.

LRU with capacity 3
Step 1 / 4
put A
put B
put C
List: C B A

STEP 1Three puts fill the cache. Most recent at the front: C, B, A.

07

Operation trace

LRU capacity 2

Step 1 / 5
OperationResultOrder (MRU -> LRU)
put(1, 'a')-1
put(2, 'b')-2, 1
get(1)'a'1, 2
put(3, 'c')evicts 23, 1
get(2)miss3, 1

NOWOperation: put(1, 'a') | Result: - | Order (MRU -> LRU): 1

Every operation is O(1): one map lookup and a constant number of pointer changes.

08

Implementation

import java.util.*;import java.util.concurrent.locks.ReentrantLock; public final class LruCache<K, V> {    private final class Node {        final K key; V value; Node prev, next;        Node(K key, V value) { this.key = key; this.value = value; }    }     private final int capacity;    private final Map<K, Node> map = new HashMap<>();    private final Node head = new Node(null, null), tail = new Node(null, null); // sentinels    private final ReentrantLock lock = new ReentrantLock();    private long hits, misses;     public LruCache(int capacity) {        if (capacity <= 0) throw new IllegalArgumentException("capacity must be positive");        this.capacity = capacity;        head.next = tail; tail.prev = head;    }     public Optional<V> get(K key) {        lock.lock();        try {            Node n = map.get(key);            if (n == null) { misses++; return Optional.empty(); }            hits++;            moveToFront(n);            return Optional.of(n.value);        } finally { lock.unlock(); }    }     public void put(K key, V value) {        lock.lock();        try {            Node n = map.get(key);            if (n != null) { n.value = value; moveToFront(n); return; }            if (map.size() == capacity) {                Node lru = tail.prev;                unlink(lru);                map.remove(lru.key);            }            n = new Node(key, value);            map.put(key, n);            addFront(n);        } finally { lock.unlock(); }    }     public double hitRate() { long total = hits + misses; return total == 0 ? 0 : (double) hits / total; }     private void moveToFront(Node n) { unlink(n); addFront(n); }    private void unlink(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }    private void addFront(Node n) { n.next = head.next; n.prev = head; head.next.prev = n; head.next = n; }}
09

Complexity and performance

get / putO(1)

Map + linked list.

SpaceO(capacity)

Node overhead per entry.

10

Trade-offs

LRU vs LFU

LRU adapts quickly to changing access patterns; LFU keeps long-term popular items but reacts slowly and needs frequency buckets.

Global lock vs striping

A single lock is simple; high-concurrency caches shard into segments with separate locks.

11

Variants and related techniques

LFU in O(1)

Frequency buckets of doubly linked lists plus a min-frequency pointer.

Write-through / write-back

When the cache fronts a store, decide how writes propagate.

12

Common mistakes

  • Using an array or list scan for recency.

    Fix: That makes operations O(n); use a linked list with node references.

  • Forgetting to update the value and recency on put of an existing key.

    Fix: Update and move to front.

  • Not removing the evicted key from the map.

    Fix: Store the key in the node so eviction can delete it.

13

Interview questions

How do you get O(1) for both get and put in an LRU cache?

A hash map gives O(1) lookup of the node; a doubly linked list lets you move a node to the front and remove the tail in O(1) because you already hold node references.

How would you support multiple eviction policies?

Extract an EvictionPolicy interface (on access, on remove, choose victim) and inject LRU, LFU, or FIFO implementations into a generic cache (Strategy pattern).

14

Practice problems

ProblemDifficultyWhat it trains
LRU CacheMediumMap + doubly linked list.
LFU CacheHardFrequency buckets.
Thread-safe TTL cache with statsHardConcurrency and expiry.