Caching in Java & Spring Boot — From Beginner to 4-Year-Experience Level
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
DatabaseA request comes:
GET /users/100Your 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
ReturnThe 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/locationIf the same data is requested repeatedly:
Without cache:
Request
↓
Application
↓
Database
↓
ResponseWith cache:
Request
↓
Application
↓
Cache
↓
ResponseSo 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 lost4. 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
+
concurrencyCaffeine 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
| |
└─────────┬─────────┘
↓
RedisThat’s one of the biggest differences between Caffeine and Redis.
6. Caffeine vs Redis
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 = 3We insert:
A
B
CCache:
A B CNow cache is full.
We access:
AA becomes the most recently used.
Then:
Bbecomes the least recently used.
Now insert:
DWho should be removed?
Bbecause 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 ListThe HashMap gives:
Key → NodeThe linked list maintains:
Most Recently Used
↓
Node
↓
Node
↓
Node
↓
Least Recently UsedFor example:
HashMapA ───────────────┐
B ───────────────┼──> Nodes
C ───────────────┘
Doubly Linked ListHEAD TAIL
↓ ↓
[A] ⇄ [C] ⇄ [B]
↑ ↑
MRU LRU
When we access A:
Before:HEAD
↓
[A] ⇄ [C] ⇄ [B]
↑
LRUAfter 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 → DYou want to remove C.
With a singly linked list, you need to know the node before C:
B → CSo you may need to traverse from the beginning:
A → B → CThat’s potentially:
O(n)With a doubly linked list, C already knows:
previous = B
next = DSo removal is:
B.next = D
D.prev = BThat’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 Cache12. 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 node13. 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 nodewe have:
HEAD ⇄ actual nodes ⇄ TAIL14. 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 ⇄ TAILAdding C:
HEAD ⇄ C ⇄ A ⇄ B ⇄ TAILC 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 ⇄ CRemove B:
A.next = C
C.prev = AResult:
A ⇄ CO(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 → TAILTherefore:
tail.previs 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 valueEverything 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:
3put(1,A)
HEAD → 1 → TAILput(2,B)
HEAD → 2 → 1 → TAILput(3,C)
HEAD → 3 → 2 → 1 → TAILNow:
MRU = 3
LRU = 1get(1)
1 becomes MRU:
HEAD → 1 → 3 → 2 → TAILNow:
MRU = 1
LRU = 2put(4,D)
Capacity exceeded.
Remove:
2Result:
HEAD → 4 → 1 → 3 → TAILThat’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:
HashMapis not thread-safe.
More importantly, even if you replaced it with:
ConcurrentHashMapthe 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:
“ConcurrentHashMapmakes 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 timesLFU would remove:
Bbecause:
frequency(B) = 227. LRU vs LFU
LRU
Based on:
recencyExample:
A was used 1 second ago
B was used 10 minutes agoB is more likely to be evicted.
LFU
Based on:
frequencyExample:
A accessed 100 times
B accessed 2 timesB is evicted.
28. Can we implement LFU with just a HashMap?
Not efficiently.
We need to track:
key → nodeand:
frequency → nodesA common O(1) LFU design uses:
HashMap<key, Node>
+
HashMap<frequency, DoublyLinkedList>
+
minFrequencyThink about it like this:
keyMapA → Node
B → Node
C → NodeAnd:
frequencyMap1 → [B, D]
2 → [C]
3 → [A, E]And:
minFrequency = 1Therefore we immediately know:
frequency 1contains 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 = 130. Frequency map
Map<Integer, DoublyLinkedList> frequencyMap;Example:
frequencyMap1 → B ⇄ D
2 → C
3 → A ⇄ ENow suppose:
get(B)B moves from:
frequency 1to:
frequency 2So:
1 → D
2 → B ⇄ C
3 → A ⇄ E31. 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 5Capacity 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:
frequencySecondary:
recencyThat’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
Caffeineor:
Spring Boot
|
v
RedisSpring provides an abstraction:
@Cacheable
@CacheEvict
@CachePut34. @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
↓
ReturnSecond request:
getUser(100)
↓
Cache HIT
↓
Return directlyThe 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 cacheThen the next read:
Cache MISS
↓
DB
↓
Fresh data
↓
Cache36. 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 minutesAfter 10 minutes:
cache entry expires2. Explicit invalidation
When data changes:
UPDATE database
↓
DELETE cache entry3. Update cache
When database changes:
UPDATE DB
↓
UPDATE CACHEEach 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 contentBad 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
ResponsePseudo-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
|
└── DatabaseRequest:
L1 Caffeine
↓ miss
L2 Redis
↓ miss
DatabaseThen:
Database
↓
Redis
↓
CaffeineNext request can be extremely fast.
40. Why have two caches?
Because each solves a different problem.
Caffeine
local
very fast
no networkRedis
shared
distributed
accessible by multiple instancesSo:
L1 = speed
L2 = shared cacheThis is a more advanced architecture.
41. Cache stampede
Imagine:
Cache entry expiresAt exactly the same time, 1,000 requests arrive.
All 1,000 see:
CACHE MISSAll 1,000 hit:
DATABASENow 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 = 999999999which doesn’t exist.
Every request:
Cache MISS
↓
Database
↓
NOT FOUNDThe attacker/requester can cause huge DB traffic.
One solution:
Cache negative results for a short period.
For example:
user:999999999 → NOT_FOUND
TTL = 30 seconds43. Cache avalanche
Imagine thousands of cache entries all have:
TTL = 10 minutesand were inserted around the same time.
They all expire around the same time.
Then:
Huge cache miss
↓
Huge DB trafficPossible solution:
TTL = 10 min + random jitterFor example:
10:03
10:07
10:11
10:15instead of everything expiring together.
44. Cache consistency
Suppose:
Database:price = ₹100Cache:
price = ₹100Now database changes:
price = ₹150but cache still says:
₹100That’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 = LRU46. 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 cachingRedis
Distributed cache/data store.
Very important for:
microservices
multiple application instances
shared cacheEhcache
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 LRUThen explain:
GET:
HashMap → find Node → move to HEAD → returnPUT existing:
HashMap → find Node → update → move to HEADPUT new:
Create Node → HashMap → HEADIf 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:
LinkedHashMapIt can maintain access order.
You can do:
LinkedHashMap<K, V> cache =
new LinkedHashMap<>(16, 0.75f, true);The true means:
accessOrder = trueThen 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 handlingIf 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 RedisThen 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.





