What is Caching?
Caching is a technique for keeping a copy of frequently used data in a fast, temporary storage area so it can be read again quickly. Instead of fetching the same data again and again from a slow source, such as a database or an external service, the application keeps a ready copy close by, usually in memory. This makes responses faster and reduces the work done by the slower system behind it.
In simple words, caching means saving data for quick reuse. Just as you save a phone number in your contacts instead of searching for it every time, applications cache data to avoid repeating the same expensive work.
1. Why Is Caching Important?
- Faster applications: reading from memory is far quicker than querying a database or calling another service.
- Less load on the database: repeated reads are answered by the cache, so the database can handle more important work.
- Better user experience: pages and APIs respond quickly, even during busy periods.
- Lower cost: fewer database queries and external API calls can mean smaller servers and lower bills.
- Higher throughput: each request finishes sooner, so the system can serve more users per second.
- Resilience: a cache can keep serving popular data for a short time even if the source is slow.
The speed difference is large. These are rough, typical numbers to show the scale:
| Where the Data Comes From | Typical Time |
|---|---|
| Application memory (local cache) | Well under a microsecond |
| Distributed cache such as Redis (same data centre) | About 0.5–1 millisecond |
| Simple indexed database query | A few milliseconds |
| Complex database query or external API call | Tens to hundreds of milliseconds |
2. Key Caching Terms
| Term | Meaning |
|---|---|
| Cache hit | The requested data is found in the cache. |
| Cache miss | The data is not in the cache, so it must be loaded from the source. |
| Hit ratio | Hits divided by total requests. 90% means 9 of every 10 reads came from the cache. |
| TTL (Time To Live) | How long an entry stays valid before it expires. |
| Eviction | Removing entries to make room when the cache is full. |
| Invalidation | Removing or updating an entry because the original data changed. |
| Stale data | A cached copy that is older than the current data in the source. |
| Cold / warm cache | A cold cache is empty (many misses); a warm cache already holds popular data. |
3. How Caching Works, Step by Step
- A user requests some data, such as an article.
- The application first checks whether the article is in the cache.
- If it is (a cache hit), the data is returned immediately.
- If it is not (a cache miss), the data is fetched from the database and saved in the cache.
- Future requests for the same article are served from the cache until it expires or changes.
Request ---> Application ---> Cache: is the data here?
|
+--------------------+--------------------+
| YES (cache hit) | NO (cache miss)
v v
Return cached data Read from database
|
v
Save a copy in cache
|
v
Return the data
4. Real-Life and Technical Examples
Real-Life Example
Imagine you visit a restaurant every day and order the same dish. On the first day the chef prepares everything from scratch, which takes time. After that, the chef keeps the sauce ready because many customers order it, so your dish arrives faster. The ready sauce is like a cache: prepared once, reused many times.
Technical Example: A Blog Website
Suppose a blog website loads each article from the database. If thousands of users open the same popular article, the database runs the same query thousands of times and may become overloaded. With caching:
- The article is fetched from the database once.
- A copy is stored in the cache (memory).
- The next users get the article directly from the cache.
- When the author edits the article, the cached copy is removed so readers see the new version.
If the article is read 10,000 times an hour and the cache hit ratio is 99%, the database handles only about 100 of those reads instead of 10,000.
5. Types of Caching
Caches exist at many layers between the user and the database:
| Type | Where It Lives | What It Stores | Examples |
|---|---|---|---|
| Browser cache | User's device | Images, CSS, JavaScript, some API responses | HTTP Cache-Control headers |
| CDN cache | Servers near the user | Static files, images, videos | CloudFront, Cloudflare |
| Reverse proxy cache | In front of the app servers | Full HTTP responses | NGINX, Varnish |
| In-memory (local) cache | Inside each application instance | Objects used often by that instance | Caffeine, ConcurrentHashMap |
| Distributed cache | Separate cache servers shared by all instances | Sessions, query results, objects | Redis, Memcached, Hazelcast |
| Database cache | Inside the database engine | Recently used data pages and query plans | PostgreSQL shared buffers, MySQL buffer pool |
Local vs distributed: a local cache is the fastest but each server has its own copy, so copies can differ between servers. A distributed cache is slightly slower (a network call) but all servers share the same data. Many systems use both: a small local cache in front of a shared Redis cache. Static content is usually best served from a CDN.
6. Caching Strategies
A caching strategy decides who loads data into the cache and when writes reach the database.
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Cache-aside (lazy loading) | The application checks the cache; on a miss it reads the database and fills the cache. | Simple; only requested data is cached | First request is slow; data can become stale |
| Read-through | The application always asks the cache; the cache itself loads missing data from the database. | Cleaner application code | Needs a cache library or provider that supports loading |
| Write-through | Every write goes to the cache and the database at the same time. | Cache is always up to date | Writes are slower; may cache data nobody reads |
| Write-behind (write-back) | Writes go to the cache first; the cache saves them to the database later. | Very fast writes | Risk of losing data if the cache fails before saving |
| Write-around | Writes go only to the database; the cache is filled later on reads. | Avoids caching data that is rarely read | First read after a write is a miss |
Cache-aside is the most common choice for web applications and is what Spring's @Cacheable does.
7. Cache Eviction Policies
Memory is limited, so when a cache is full it must remove some entries. The eviction policy decides which ones:
- LRU (Least Recently Used): removes the item that has not been used for the longest time. The most popular general-purpose choice.
- LFU (Least Frequently Used): removes the item used the fewest times. Good when some items stay popular for a long time.
- FIFO (First In, First Out): removes the oldest item, regardless of how often it is used.
- TTL-based expiry: removes items automatically after a fixed time.
- Random: removes a random item; simple and sometimes surprisingly effective.
8. Cache Invalidation
Keeping cached data correct after the source changes is one of the hardest parts of caching. Common approaches:
- Expire with a TTL: accept that data may be a little old, and let entries expire after a set time.
- Evict on write: delete the cached entry whenever the data is updated or deleted, so the next read loads the fresh value.
- Update on write: write the new value into the cache at the same time as the database.
- Event-based invalidation: when data changes, publish an event so every service and cache can remove its copy.
- Versioned keys: include a version in the key (for example
article:42:v7) so updated data uses a new key.
9. Common Caching Problems
| Problem | What Happens | Common Fix |
|---|---|---|
| Stale data | Users see old values after the data changes | Evict on write and use sensible TTLs |
| Cache stampede (thundering herd) | A popular entry expires and thousands of requests hit the database at once | Let only one request reload the value; add random variation (jitter) to TTLs |
| Cache penetration | Repeated requests for data that does not exist always miss and hit the database | Cache "not found" results briefly; validate input; use a Bloom filter |
| Cache avalanche | Many entries expire at the same moment, or the cache server goes down | Spread out expiry times; make the cache highly available |
| Hot keys | One extremely popular key overloads a single cache node | Add a small local cache in front; replicate the hot key |
| Memory pressure | The cache grows until it slows down or crashes the application | Set a maximum size and an eviction policy |
10. Caching in Java
A Simple LRU Cache
Java's LinkedHashMap can keep entries in access order and remove the oldest one automatically, which makes a tiny LRU cache:
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(16, 0.75f, true); // true = keep entries in access order, not insertion order
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // remove the least recently used entry when full
}
public static void main(String[] args) {
LruCache<String, String> cache = new LruCache<>(3);
cache.put("A", "Apple");
cache.put("B", "Banana");
cache.put("C", "Cherry");
cache.get("A"); // A is now the most recently used
cache.put("D", "Date"); // cache is full, so B (least recently used) is removed
System.out.println(cache.keySet());
}
}
[C, A, D]
This version is not thread-safe. For real applications, use a proven library such as Caffeine.
Cache-Aside with Expiry (TTL)
This small cache loads data on a miss and keeps it for a fixed time:
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public class TtlCache<K, V> {
private record Entry<V>(V value, long expiresAt) {
}
private final Map<K, Entry<V>> store = new ConcurrentHashMap<>();
private final long ttlMillis;
public TtlCache(Duration ttl) {
this.ttlMillis = ttl.toMillis();
}
public V getOrLoad(K key, Function<K, V> loader) {
long now = System.currentTimeMillis();
Entry<V> entry = store.get(key);
if (entry != null && entry.expiresAt() > now) {
System.out.println("Cache HIT for " + key);
return entry.value();
}
System.out.println("Cache MISS for " + key + " -> loading from database");
V value = loader.apply(key);
store.put(key, new Entry<>(value, now + ttlMillis));
return value;
}
public void evict(K key) {
store.remove(key); // call this when the source data changes
}
public static void main(String[] args) throws InterruptedException {
TtlCache<Long, String> articles = new TtlCache<>(Duration.ofSeconds(1));
Function<Long, String> database = id -> "Article #" + id;
articles.getOrLoad(42L, database); // miss: first request
articles.getOrLoad(42L, database); // hit: served from memory
Thread.sleep(1_100); // wait until the entry expires
articles.getOrLoad(42L, database); // miss: entry expired
articles.evict(42L); // the article was edited
articles.getOrLoad(42L, database); // miss: entry was evicted
}
}
Cache MISS for 42 -> loading from database
Cache HIT for 42
Cache MISS for 42 -> loading from database
Cache MISS for 42 -> loading from database
Caching in Spring Boot with Redis
Spring's cache annotations implement cache-aside for you. Add spring-boot-starter-cache and spring-boot-starter-data-redis, then configure Redis:
spring:
cache:
type: redis
redis:
time-to-live: 10m # every cached entry expires after 10 minutes
cache-null-values: false
data:
redis:
host: localhost
port: 6379
package com.javabytechie.blog;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ArticleService {
private final ArticleRepository articleRepository;
public ArticleService(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@Cacheable(cacheNames = "articles", key = "#id")
public Article getArticle(Long id) {
// Runs only on a cache miss; the result is stored in the "articles" cache.
return articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}
@Transactional
@CachePut(cacheNames = "articles", key = "#result.id")
public Article updateArticle(Article article) {
// Saves to the database and writes the new version into the cache.
return articleRepository.save(article);
}
@Transactional
@CacheEvict(cacheNames = "articles", key = "#id")
public void deleteArticle(Long id) {
// Removes the cached copy so nobody reads a deleted article.
articleRepository.deleteById(id);
}
}
package com.javabytechie.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // turns on @Cacheable, @CachePut and @CacheEvict
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
By default, Redis stores cached objects using Java serialization, so Article must implement Serializable, or you can configure a JSON serializer. The annotations only work when the method is called from another bean, because Spring applies caching through a proxy.
11. When to Use and When to Avoid Caching
| Use Caching When Data | Avoid Caching When Data |
|---|---|
| Is read much more often than it is written | Changes very frequently |
| Does not change very often | Must always be exactly real-time, such as an account balance during payment |
| Is slow or expensive to load or calculate | Could cause wrong results or decisions if slightly out of date |
| Is requested by many users, such as popular products | Is unique per request and rarely reused |
| Can safely be a little out of date | Is sensitive and must not be copied to shared storage |
12. Best Practices
- Cache data that is read often, changes rarely and is costly to fetch.
- Always set a TTL, even a long one, so wrong data cannot live forever.
- Set a maximum size and choose an eviction policy, usually LRU.
- Evict or update cache entries whenever the source data changes.
- Use clear, consistent key names, such as
article:42. - Add small random variation to TTLs to avoid many entries expiring together.
- Protect popular entries from cache stampedes by letting one request reload them.
- Treat the cache as optional: the application must still work if the cache is empty or down.
- Monitor hit ratio, memory use and eviction counts.
- Never cache secrets or personal data in shared caches without encryption and access control.
13. Summary
Caching is a powerful technique that improves application performance by keeping frequently used data in fast, temporary storage. Caches exist at many levels, from the browser and CDN to in-memory and distributed caches such as Redis. Strategies like cache-aside, write-through and write-behind decide how data enters the cache, while eviction policies like LRU decide what leaves it. The hardest part is keeping cached data correct, which is handled with TTLs and invalidation. Used wisely, caching makes applications faster, reduces database load and helps systems scale.
Frequently Asked Questions (FAQ)
Q1. What is caching in simple words?
Caching means keeping a copy of frequently used data in a fast storage area, usually memory, so the application can reuse it quickly instead of loading it again from a slow source such as a database.
Q2. What is a cache hit and a cache miss?
A cache hit happens when the requested data is already in the cache and is returned immediately. A cache miss happens when it is not there, so the application must load it from the original source and usually stores it in the cache for next time.
Q3. What is a cache hit ratio, and what is a good value?
It is the share of requests served from the cache: hits divided by total requests. A good ratio depends on the use case, but many read-heavy systems aim for 80–95% or more. A low ratio may mean the wrong data is cached or TTLs are too short.
Q4. What is TTL in caching?
TTL (Time To Live) is how long a cached entry stays valid. After the TTL ends, the entry expires and the next request loads fresh data. Short TTLs keep data fresher; long TTLs give more cache hits.
Q5. What is the difference between a local cache and a distributed cache?
A local cache lives inside one application instance; it is extremely fast, but each server has its own copy. A distributed cache, such as Redis, runs on separate servers shared by all instances; it needs a network call but keeps one consistent copy for everyone.
Q6. What is the cache-aside pattern?
The application checks the cache first. On a miss, it reads the database, stores the result in the cache and returns it. On updates, it changes the database and removes the cached entry. It is the most widely used caching strategy.
Q7. What is the difference between write-through and write-behind caching?
With write-through, each write updates the cache and the database together, so both are always in sync. With write-behind, writes update the cache first and the database later, which is faster but can lose data if the cache fails before saving.
Q8. What is an LRU cache?
An LRU (Least Recently Used) cache removes the entry that has gone unused for the longest time when it needs space. It works well because recently used data is likely to be used again soon.
Q9. What is the difference between LRU and LFU?
LRU removes the item used least recently; LFU removes the item used least often. LFU keeps long-term favourites even if they were not used in the last few minutes, while LRU adapts faster to changing popularity.
Q10. What is cache invalidation, and why is it hard?
Cache invalidation means removing or updating cached data when the original data changes. It is hard because data can change in many places, several caches may hold copies, and timing issues can leave old values behind.
Q11. What is stale data?
Stale data is a cached copy that no longer matches the source. For example, the cache still shows an old price after the price was updated. TTLs and evicting entries on writes reduce stale data.
Q12. What is a cache stampede?
It happens when a very popular entry expires and many requests miss at the same moment, all hitting the database together. Fixes include letting only one request reload the value while others wait, refreshing popular entries early, and adding random jitter to TTLs.
Q13. What is cache penetration?
It happens when requests ask for data that does not exist, such as random IDs. Each request misses the cache and reaches the database. Caching "not found" results for a short time, validating input and using a Bloom filter help prevent it.
Q14. Is Redis a cache or a database?
Redis is an in-memory data store that can be used as both. It is most often used as a distributed cache, but it also supports persistence, data structures, pub/sub messaging and more.
Q15. What is the difference between Redis and Memcached?
Both are fast in-memory caches. Memcached is simple and focused on key-value caching. Redis offers richer data types (lists, sets, hashes), optional persistence, replication and built-in features such as pub/sub, which is why it is more commonly chosen today.
Q16. How do I enable caching in Spring Boot?
Add spring-boot-starter-cache, put @EnableCaching on a configuration class, and annotate methods:
@Cacheable(cacheNames = "articles", key = "#id")
public Article getArticle(Long id) {
return articleRepository.findById(id).orElseThrow();
}
Add a provider such as Redis or Caffeine to control where data is stored.
Q17. What is the difference between @Cacheable, @CachePut and @CacheEvict?
- @Cacheable: returns the cached value if present; otherwise runs the method and caches the result.
- @CachePut: always runs the method and updates the cache with the result.
- @CacheEvict: removes one or all entries from the cache.
Q18. What is browser caching?
Browsers store static files such as images, CSS and JavaScript on the user's device. Servers control this with HTTP headers like Cache-Control and ETag, so repeat visits load much faster.
Q19. Can caching cause problems?
Yes. It can serve outdated data, use too much memory, hide database problems until the cache is cleared, and add complexity. A cache server can also become a point of failure, so the application must keep working if the cache is unavailable.
Q20. What kind of data should not be cached?
Data that changes constantly, data that must always be exactly current (such as stock levels during checkout or account balances during payment), data that is unique to each request, and sensitive data that should not be copied to shared storage.