Sitemap
curiouswomen

curiouswomen

I am here to share my experience being it a technical, non-technical or daily life experience that I am curious and thrive a positive change.

Caching in Java & Spring Boot — From Beginner to 4-Year-Experience Level

16 min readSep 7, 2026

--

If you’re a Java/Spring Boot developer with around 4 years of experience, caching is one of those topics where interviewers don’t just want to hear “Redis is used for caching.”

They want to know:

  • Why do we need caching?
  • Where does cached data actually live?
  • What should we cache?
  • When should we use Caffeine vs Redis?
  • What is LRU/LFU?
  • How do we implement LRU ourselves?
  • Why HashMap + Doubly Linked List?
  • How do we achieve O(1)?
  • Is it thread-safe?
  • What happens when data becomes stale?
  • What happens when multiple application instances are running?
  • What happens when Redis goes down?
  • How do we prevent cache stampede?
  • How do we monitor cache performance?

Let’s build the understanding from zero → interview-ready → architecture-level thinking.

1. First: What exactly is caching?

Suppose we have:

Client
|
v
Spring Boot Application
|
v
Database

A request comes:

GET /users/100

Your application does:

User user = userRepository.findById(100);

The database has to process the request.

If this user is requested 10,000 times, we potentially hit the database 10,000 times.

That’s unnecessary if the data doesn’t change frequently.

So we introduce a cache:

┌──────────────┐
│ Client │
└──────┬───────┘
│
v
┌──────────────────┐
│ Spring Boot App │
└────────┬─────────┘
│
Check Cache
│
┌────────┴────────┐
│ │
HIT MISS
│ │
v v
Return Database
quickly │
│
v
Put in Cache
│
v
Return

The fundamental idea is:

Cache = temporarily store frequently accessed data closer to the application so that we don’t repeatedly perform an expensive operation.

2. Why is caching useful?

Consider:

Database query = 20 ms
Cache lookup = ~microseconds to low milliseconds depending on cache/location

If the same data is requested repeatedly:

Without cache:

Request
↓
Application
↓
Database
↓
Response

With cache:

Request
↓
Application
↓
Cache
↓
Response

So caching can improve:

  • latency
  • throughput
  • database load
  • scalability
  • user experience

But caching is not free.

It introduces new problems:

  • stale data
  • invalidation
  • memory consumption
  • cache misses
  • cache stampede
  • consistency issues
  • distributed-cache failures

That’s why caching is a system-design topic, not just a library feature.

3. Where does the cache actually live?

This is extremely important.

There are two common types you’ll encounter in Spring Boot.

A. Local/In-memory cache

Example:

Caffeine

The cache lives inside your application’s JVM memory.

Spring Boot Application
┌─────────────────────────────┐
│ JVM Heap │
│ │
│ Cache │
│ ┌──────┬──────┬──────┐ │
│ │user1 │user2 │user3 │ │
│ └──────┴──────┴──────┘ │
│ │
└─────────────────────────────┘

If your application restarts:

Application Restart
↓
JVM memory cleared
↓
Cache lost

4. What is Caffeine?

Caffeine is a popular Java in-process caching library.

It provides things we would otherwise have to implement ourselves:

  • thread safety
  • maximum size
  • expiration
  • eviction
  • efficient concurrent access
  • statistics

For example:

Cache<Long, User> cache =
Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();

Then:

cache.put(100L, user);

and:

User user = cache.getIfPresent(100L);

You don’t have to manually maintain:

HashMap
+
Doubly Linked List
+
TTL
+
eviction
+
concurrency

Caffeine handles those concerns.

5. What is Redis?

Redis is different.

Redis is an external in-memory data store.

Your application and Redis are separate processes.

┌──────────────────┐
│ Spring Boot App │
└────────┬─────────┘
│
Network call
│
v
┌──────────────────┐
│ Redis │
│ │
│ user:100 → data │
│ user:101 → data │
└──────────────────┘

This means multiple application instances can share the same cache.

Load Balancer
|
┌─────────┴─────────┐
↓ ↓
App Instance 1 App Instance 2
| |
└─────────┬─────────┘
↓
Redis

That’s one of the biggest differences between Caffeine and Redis.

6. Caffeine vs Redis

Press enter or click to view image in full size

A strong interview answer:

“If I have a single application instance or want an extremely fast local cache, I can use Caffeine. If I have multiple application instances and need a shared cache, Redis is more appropriate.”

7. Now understand the cache itself

Suppose the interviewer says:

Build a cache with maximum capacity 3.

We have:

Capacity = 3

We insert:

A
B
C

Cache:

A B C

Now cache is full.

We access:

A

A becomes the most recently used.

Then:

B

becomes the least recently used.

Now insert:

D

Who should be removed?

B

because B hasn’t been used recently.

This is:

LRU — Least Recently Used

8. Why can’t we simply use HashMap?

A HashMap gives us:

map.get(key)

in average:

O(1)

Great.

But HashMap doesn’t maintain the order we need.

We need to know:

Who was used most recently?
Who was used least recently?

A HashMap doesn’t naturally give us that information.

So we need another data structure.

9. Why Doubly Linked List?

We use:

HashMap + Doubly Linked List

The HashMap gives:

Key → Node

The linked list maintains:

Most Recently Used
↓
Node
↓
Node
↓
Node
↓
Least Recently Used

For example:

HashMap
A ───────────────┐
B ───────────────┼──> Nodes
C ───────────────┘
Doubly Linked ListHEAD TAIL
↓ ↓
[A] ⇄ [C] ⇄ [B]
↑ ↑
MRU LRU

When we access A:

Before:
HEAD
↓
[A] ⇄ [C] ⇄ [B]
↑
LRU

After accessing A, A is already MRU.

If we access B:

Before:
[A] ⇄ [C] ⇄ [B]After:[B] ⇄ [A] ⇄ [C]
↑ ↑
MRU LRU

That’s why we need a doubly linked list.

10. Why not singly linked list?

This is a very common interview follow-up.

Suppose:

A → B → C → D

You want to remove C.

With a singly linked list, you need to know the node before C:

B → C

So you may need to traverse from the beginning:

A → B → C

That’s potentially:

O(n)

With a doubly linked list, C already knows:

previous = B
next = D

So removal is:

B.next = D
D.prev = B

That’s:

O(1)

This is the key reason.

11. The magic combination

This is the most important thing to remember:

HashMap

Provides:

Find node → O(1)

Doubly Linked List

Provides:

Remove node → O(1)
Move node → O(1)
Insert node → O(1)

Together:

HashMap
+
Doubly Linked List
=
O(1) LRU Cache

12. Let’s implement LRU in Java

We’ll intentionally not use Caffeine because this is what you should understand for an interview.

Node

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;
}
}

Each node contains:

key
value
previous node
next node

13. LRU Cache

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;    private final Node<K, V> head;
private final Node<K, V> tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
head = new Node<>(null, null);
tail = new Node<>(null, null);
head.next = tail;
tail.prev = head;
}
}

Why are we creating dummy head and tail nodes?

It simplifies insertion and deletion.

Instead of repeatedly handling:

if first node
if last node
if only node

we have:

HEAD ⇄ actual nodes ⇄ TAIL

14. Add node at the front

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

So:

HEAD ⇄ A ⇄ B ⇄ TAIL

Adding C:

HEAD ⇄ C ⇄ A ⇄ B ⇄ TAIL

C is now MRU.

15. Remove a node

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

Suppose:

A ⇄ B ⇄ C

Remove B:

A.next = C
C.prev = A

Result:

A ⇄ C

O(1).

16. Move node to front

private void moveToFront(Node<K, V> node) {
    removeNode(node);
addToFront(node);
}

Very simple.

17. Remove least recently used item

The LRU item is just before tail.

private Node<K, V> removeLRU() {
    Node<K, V> lru = tail.prev;    removeNode(lru);    return lru;
}

Why?

Because our list is:

HEAD → MRU → ... → LRU → TAIL

Therefore:

tail.prev

is the LRU node.

18. Implement get()

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

Flow:

get(A)
↓
HashMap
↓
Find Node A
↓
Move A to HEAD
↓
Return value

Everything is O(1).

19. Implement put()

public void put(K key, V value) {
    Node<K, V> existingNode = cache.get(key);    if (existingNode != null) {        existingNode.value = value;        moveToFront(existingNode);        return;
}
Node<K, V> newNode = new Node<>(key, value); cache.put(key, newNode); addToFront(newNode); if (cache.size() > capacity) { Node<K, V> lru = removeLRU(); cache.remove(lru.key);
}
}

And that’s essentially the complete LRU cache.

20. Complete LRU code

import java.util.HashMap;
import java.util.Map;
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;
}
}
public class LRUCache<K, V> { private final int capacity; private final Map<K, Node<K, V>> cache; private final Node<K, V> head;
private final Node<K, V> tail;
public LRUCache(int capacity) { if (capacity <= 0) {
throw new IllegalArgumentException("Capacity must be positive");
}
this.capacity = capacity;
this.cache = new HashMap<>();
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;
}
Node<K, V> newNode = new Node<>(key, value); cache.put(key, newNode); addToFront(newNode); if (cache.size() > capacity) { Node<K, V> lru = removeLRU(); cache.remove(lru.key);
}
}
private void addToFront(Node<K, V> node) { node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node<K, V> node) { node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToFront(Node<K, V> node) { removeNode(node);
addToFront(node);
}
private Node<K, V> removeLRU() { Node<K, V> lru = tail.prev; removeNode(lru); return lru;
}
}

21. Let’s execute it mentally

Capacity:

3

put(1,A)

HEAD → 1 → TAIL

put(2,B)

HEAD → 2 → 1 → TAIL

put(3,C)

HEAD → 3 → 2 → 1 → TAIL

Now:

MRU = 3
LRU = 1

get(1)

1 becomes MRU:

HEAD → 1 → 3 → 2 → TAIL

Now:

MRU = 1
LRU = 2

put(4,D)

Capacity exceeded.

Remove:

2

Result:

HEAD → 4 → 1 → 3 → TAIL

That’s LRU.

22. Complexity

OperationComplexitygetO(1)putO(1)removeO(1)insertO(1)evictionO(1)SpaceO(capacity)

This is exactly what the interviewer is looking for.

23. But is our implementation thread-safe?

No.

This is an important 4-year-level follow-up.

Our:

HashMap

is not thread-safe.

More importantly, even if you replaced it with:

ConcurrentHashMap

the whole operation would still not automatically become thread-safe.

Why?

Because an LRU operation involves multiple structures.

For example:

cache.get(key);
moveToFront(node);

That is multiple operations.

Another thread could modify the linked list between those operations.

The HashMap and linked list must remain consistent with each other.

24. Simple way to make our LRU thread-safe

For an interview, the simplest correct approach is a ReentrantLock.

private final ReentrantLock lock = new ReentrantLock();

Then:

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();
}
}

And:

public void put(K key, V value) {
    lock.lock();    try {        // modify HashMap + linked list    } finally {        lock.unlock();
}
}

Now the compound operation is protected.

25. Important interview point: Why not just ConcurrentHashMap?

This is a great follow-up.

Get curiouswomen’s stories in your inbox

Join Medium for free to get updates from this writer.

You can say:

“ConcurrentHashMap makes individual map operations thread-safe, but an LRU cache is a compound data structure consisting of a map and a linked list. Operations such as get involve both looking up the node and modifying its position in the list. Therefore I need synchronization or locking around the complete operation to maintain consistency."

That’s a very good 4-year-level answer.

26. Now let’s understand LFU

LRU asks:

Which item was used least recently?

LFU asks:

Which item was used least frequently?

Example:

A → accessed 10 times
B → accessed 2 times
C → accessed 7 times

LFU would remove:

B

because:

frequency(B) = 2

27. LRU vs LFU

LRU

Based on:

recency

Example:

A was used 1 second ago
B was used 10 minutes ago

B is more likely to be evicted.

LFU

Based on:

frequency

Example:

A accessed 100 times
B accessed 2 times

B is evicted.

28. Can we implement LFU with just a HashMap?

Not efficiently.

We need to track:

key → node

and:

frequency → nodes

A common O(1) LFU design uses:

HashMap<key, Node>
+
HashMap<frequency, DoublyLinkedList>
+
minFrequency

Think about it like this:

keyMap
A → Node
B → Node
C → Node

And:

frequencyMap
1 → [B, D]
2 → [C]
3 → [A, E]

And:

minFrequency = 1

Therefore we immediately know:

frequency 1

contains the candidates for eviction.

29. LFU Node

class LFUNode {
    int key;
int value;
int frequency; LFUNode prev;
LFUNode next;
LFUNode(int key, int value) {
this.key = key;
this.value = value;
this.frequency = 1;
}
}

Every new item starts with:

frequency = 1

30. Frequency map

Map<Integer, DoublyLinkedList> frequencyMap;

Example:

frequencyMap
1 → B ⇄ D
2 → C
3 → A ⇄ E

Now suppose:

get(B)

B moves from:

frequency 1

to:

frequency 2

So:

1 → D
2 → B ⇄ C
3 → A ⇄ E

31. LFU implementation

Here’s the standard interview-style structure:

import java.util.HashMap;
import java.util.Map;
public class LFUCache {    private final int capacity;    private int minFrequency;    private final Map<Integer, LFUNode> keyMap;    private final Map<Integer, DoublyLinkedList> frequencyMap;    public LFUCache(int capacity) {        this.capacity = capacity;        keyMap = new HashMap<>();
frequencyMap = new HashMap<>();
}
public int get(int key) { LFUNode node = keyMap.get(key); if (node == null) {
return -1;
}
increaseFrequency(node); return node.value;
}
public void put(int key, int value) { if (capacity == 0) {
return;
}
LFUNode existing = keyMap.get(key); if (existing != null) { existing.value = value; increaseFrequency(existing); return;
}
if (keyMap.size() >= capacity) { DoublyLinkedList list = frequencyMap.get(minFrequency); LFUNode removed = list.removeLast(); keyMap.remove(removed.key);
}
LFUNode newNode = new LFUNode(key, value); keyMap.put(key, newNode); frequencyMap
.computeIfAbsent(1, k -> new DoublyLinkedList())
.addFirst(newNode);
minFrequency = 1;
}
private void increaseFrequency(LFUNode node) { int oldFrequency = node.frequency; DoublyLinkedList oldList =
frequencyMap.get(oldFrequency);
oldList.remove(node); if (oldFrequency == minFrequency &&
oldList.isEmpty()) {
minFrequency++;
}
node.frequency++; frequencyMap
.computeIfAbsent(
node.frequency,
k -> new DoublyLinkedList())
.addFirst(node);
}
}

The linked list implementation:

class DoublyLinkedList {
    private final LFUNode head;
private final LFUNode tail;
DoublyLinkedList() { head = new LFUNode(-1, -1);
tail = new LFUNode(-1, -1);
head.next = tail;
tail.prev = head;
}
void addFirst(LFUNode node) { node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
void remove(LFUNode node) { node.prev.next = node.next;
node.next.prev = node.prev;
}
LFUNode removeLast() { LFUNode node = tail.prev; remove(node); return node;
}
boolean isEmpty() { return head.next == tail;
}
}

32. Why does LFU need LRU as a tie-breaker?

This is a very important interview follow-up.

Suppose:

A → frequency 2
B → frequency 2
C → frequency 5

Capacity is full.

Who do we remove?

Both A and B have the same frequency.

So we need another rule.

Usually:

Among items with the same frequency, evict the least recently used item.

So LFU becomes:

Primary:
frequency
Secondary:
recency

That’s why the LFU implementation becomes more complicated.

33. Now come back to real Spring Boot

In production, you normally don’t manually implement the cache unless the requirement specifically asks you to.

For example:

Spring Boot
|
v
Caffeine

or:

Spring Boot
|
v
Redis

Spring provides an abstraction:

@Cacheable
@CacheEvict
@CachePut

34. @Cacheable

Suppose:

@Cacheable("users")
public User getUser(Long id) {
    return userRepository.findById(id)
.orElseThrow();
}

First request:

getUser(100)
↓
Cache MISS
↓
Database
↓
Store result in cache
↓
Return

Second request:

getUser(100)
↓
Cache HIT
↓
Return directly

The method may not even execute on a cache hit.

35. @CacheEvict

Suppose user changes:

@CacheEvict(
value = "users",
key = "#id"
)
public void updateUser(Long id, UserRequest request) {
    // update database
}

Flow:

Update DB
↓
Evict cache

Then the next read:

Cache MISS
↓
DB
↓
Fresh data
↓
Cache

36. Cache invalidation

This is one of the famous engineering problems.

There are three classic approaches.

1. TTL

Data expires after some time.

Example:

TTL = 10 minutes

After 10 minutes:

cache entry expires

2. Explicit invalidation

When data changes:

UPDATE database
↓
DELETE cache entry

3. Update cache

When database changes:

UPDATE DB
↓
UPDATE CACHE

Each strategy has tradeoffs.

37. What should you cache?

Don’t blindly cache everything.

Good candidates:

  • frequently accessed
  • expensive to calculate
  • relatively stable
  • read-heavy

Examples:

Product details
User profile
Configuration
Reference/master data
Exchange rates
Permissions
Frequently viewed content

Bad candidates can include:

  • highly volatile data
  • huge objects
  • sensitive information without proper controls
  • data where stale values are unacceptable

38. Cache-aside pattern

This is probably the most important caching pattern to understand.

Application
|
v
Check Cache
/ \
HIT MISS
| |
| v
| Database
| |
| v
| Cache
| |
└────────┘
|
v
Response

Pseudo-code:

public Product getProduct(Long id) {
    Product product = cache.get(id);    if (product != null) {
return product;
}
product = database.find(id); cache.put(id, product); return product;
}

This is called:

Cache-aside / Lazy Loading

39. Multi-level caching

Now you’re thinking at architecture level.

Suppose we have:

Client
|
v
Load Balancer
|
v
Spring Boot
|
├── L1 Cache: Caffeine
|
├── L2 Cache: Redis
|
└── Database

Request:

L1 Caffeine
↓ miss
L2 Redis
↓ miss
Database

Then:

Database
↓
Redis
↓
Caffeine

Next request can be extremely fast.

40. Why have two caches?

Because each solves a different problem.

Caffeine

local
very fast
no network

Redis

shared
distributed
accessible by multiple instances

So:

L1 = speed
L2 = shared cache

This is a more advanced architecture.

41. Cache stampede

Imagine:

Cache entry expires

At exactly the same time, 1,000 requests arrive.

All 1,000 see:

CACHE MISS

All 1,000 hit:

DATABASE

Now your database gets hammered.

This is called:

Cache Stampede / Thundering Herd

Solutions include:

  • locking
  • request coalescing
  • refresh-ahead
  • randomized TTL
  • distributed locks in some architectures

42. Cache penetration

Another problem.

Suppose someone repeatedly requests:

userId = 999999999

which doesn’t exist.

Every request:

Cache MISS
↓
Database
↓
NOT FOUND

The attacker/requester can cause huge DB traffic.

One solution:

Cache negative results for a short period.

For example:

user:999999999 → NOT_FOUND
TTL = 30 seconds

43. Cache avalanche

Imagine thousands of cache entries all have:

TTL = 10 minutes

and were inserted around the same time.

They all expire around the same time.

Then:

Huge cache miss
↓
Huge DB traffic

Possible solution:

TTL = 10 min + random jitter

For example:

10:03
10:07
10:11
10:15

instead of everything expiring together.

44. Cache consistency

Suppose:

Database:
price = ₹100

Cache:

price = ₹100

Now database changes:

price = ₹150

but cache still says:

₹100

That’s stale data.

Therefore whenever designing caching, ask:

“How much stale data can the business tolerate?”

For a product catalog:

A few seconds/minutes may be okay.

For a financial transaction:

Stale data may be unacceptable.

Caching strategy depends heavily on the business requirement.

45. Cache eviction policies you should know

LRU

Least Recently Used.

Remove the item that hasn't been used recently.

LFU

Least Frequently Used.

Remove the item used the fewest times.

FIFO

First In, First Out.

Remove the oldest inserted item.

TTL-based expiration

Remove after a certain time.

In real systems, these can be combined.

For example:

Maximum size = 10,000
TTL = 10 minutes
Eviction = LRU

46. What third-party libraries should you know?

For a Java/Spring Boot developer, know these conceptually:

Caffeine

Local/in-process Java cache.

Very important for:

JVM-level caching

Redis

Distributed cache/data store.

Very important for:

microservices
multiple application instances
shared cache

Ehcache

Another Java caching solution, although Caffeine is a common modern choice for local caching.

Hazelcast

Distributed in-memory data platform/cache.

You don’t need to memorize every API.

Understand when and why you’d choose them.

47. What does a 4-year interviewer expect?

If they ask:

“Design a cache.”

Don’t immediately say:

“I’ll use Redis.”

Instead, think in this order:

1. What data?
↓
2. How frequently is it read?
↓
3. How frequently does it change?
↓
4. How much stale data is acceptable?
↓
5. How much data?
↓
6. Single instance or multiple instances?
↓
7. TTL?
↓
8. Eviction policy?
↓
9. Consistency strategy?
↓
10. Failure strategy?
↓
11. Monitoring?

That is architecture thinking.

48. Your exact interview question

If they say:

“Build an in-memory cache with maximum 10 items. Use LRU eviction. All operations should be O(1).”

Your answer can start like this:

“First, I need two requirements: O(1) lookup and O(1) eviction/reordering. A HashMap gives me O(1) average lookup, but it doesn’t maintain recency. A doubly linked list gives me O(1) insertion, deletion, and movement of a known node. Therefore I’ll combine a HashMap with a doubly linked list.”

Then draw:

HashMap
key → Node
|
v
HEAD ⇄ [Node] ⇄ [Node] ⇄ [Node] ⇄ TAIL
MRU LRU

Then explain:

GET:
HashMap → find Node → move to HEAD → return
PUT existing:
HashMap → find Node → update → move to HEAD
PUT new:
Create Node → HashMap → HEAD
If capacity exceeded:
TAIL.prev → remove → remove from HashMap

Then:

get = O(1)
put = O(1)
eviction = O(1)

That is already a strong answer.

49. Then the interviewer asks: “Is it thread-safe?”

You say:

“My basic implementation isn’t thread-safe because both the HashMap and linked list are modified. I would protect the compound cache operations using a ReentrantLock or synchronized block. Simply using ConcurrentHashMap isn’t sufficient because the LRU operation involves coordinated changes to both the map and linked list.”

Excellent.

50. Then: “Why not use LinkedHashMap?”

This is another very important follow-up.

Java already provides:

LinkedHashMap

It can maintain access order.

You can do:

LinkedHashMap<K, V> cache =
new LinkedHashMap<>(16, 0.75f, true);

The true means:

accessOrder = true

Then override:

removeEldestEntry()

Example:

class LRUCache<K, V>
extends LinkedHashMap<K, V> {
    private final int capacity;    public LRUCache(int capacity) {        super(capacity, 0.75f, true);        this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(
Map.Entry<K, V> eldest) {
return size() > capacity;
}
}

This is a beautiful practical Java solution.

But if the interviewer specifically asks:

“Implement using HashMap + Doubly Linked List.”

then don’t use LinkedHashMap.

They want to test whether you understand what’s happening underneath.

51. The hierarchy you should learn

Don’t try to memorize everything at once.

Learn caching in this order:

LEVEL 1
What is caching?
↓
LEVEL 2
Cache hit / miss
↓
LEVEL 3
In-memory cache
↓
LEVEL 4
HashMap
↓
LEVEL 5
LRU
↓
LEVEL 6
HashMap + Doubly Linked List
↓
LEVEL 7
LFU
↓
LEVEL 8
Thread safety
↓
LEVEL 9
Caffeine
↓
LEVEL 10
Redis
↓
LEVEL 11
Cache-aside
↓
LEVEL 12
TTL + eviction
↓
LEVEL 13
Consistency / invalidation
↓
LEVEL 14
Stampede / penetration / avalanche
↓
LEVEL 15
Distributed caching architecture
↓
LEVEL 16
Monitoring + failure handling

If you understand this progression, you’re no longer just memorizing “Redis is a cache.”

You’re actually understanding caching as an architecture.

52. The one mental model I want you to remember

Whenever you hear CACHE, immediately think:

CACHE
|
┌───────────┼───────────┐
↓ ↓ ↓
WHERE? WHAT? HOW?
| | |
Caffeine Data LRU/LFU
Redis Size TTL
Read FIFO
Write
|
↓
DISTRIBUTED?
|
┌───┴────┐
No Yes
| |
Caffeine Redis

Then ask:

Consistency?
Expiration?
Eviction?
Thread safety?
Failure?
Monitoring?

That mental checklist will take you from beginner caching questions to 4-year system-design discussions.

And for your specific interview question, the most important implementation to be able to write without looking anywhere is:

HashMap + Doubly Linked List → LRU → O(1) get/put/eviction → then explain how you’d make it thread-safe.

That is the foundation. LFU is the next step once LRU is completely comfortable.

Press enter or click to view image in full size
Press enter or click to view image in full size

--

--

Written by curiouswomen

I am here to share my experience being it a technical, non-technical or daily life experience that I am curious and thrive a positive change.