Building High-Performance Distributed Caching with Redis in Spring Boot

Introduction

Modern applications often retrieve the same data repeatedly from databases. Product information, customer profiles, configuration settings, and reference data are commonly accessed by multiple users and services.

Without caching, every request travels through the application layers and eventually reaches the database, increasing response times and placing unnecessary load on backend systems.

Redis provides a highly performant in-memory data store that allows applications to serve frequently requested data in milliseconds while dramatically reducing database load.

In this article, we will explore:

  • Redis fundamentals
  • Cache design patterns
  • Spring Boot caching implementation
  • Cache invalidation strategies
  • Cache Stampede prevention
  • Production best practices

Understanding the Spring Boot Request Flow

A typical Spring Boot application follows the layered architecture below:

Client
   |
Controller
   |
Service
   |
Repository
   |
Database

Without caching, every request eventually reaches the database.

Example:

GET /products/1001

Controller
      |
ProductService
      |
ProductRepository
      |
Database

For frequently accessed data, this becomes inefficient.

Redis can be introduced between the Service Layer and Repository Layer.

Client
   |
Controller
   |
Service
   |
Redis Cache
   |
Repository
   |
Database

Now the service first checks Redis before querying the database.


Why Redis?

Redis is an in-memory key-value data store designed for ultra-low latency access.

Typical response times:

OperationAverage Latency
Redis Read< 1 ms
Database Read10-100 ms
Remote Service Call50-500 ms

Benefits:

  • Faster API responses
  • Reduced database load
  • Improved scalability
  • Better user experience

Common use cases:

  • Product catalog caching
  • Customer profile caching
  • Session storage
  • Authentication data
  • Application configuration
  • Reference data

Redis Data Structures

Redis supports several data types.

Strings

product:1001 -> JSON Object

Hashes

user:1001

name=Rahul
tier=Gold
country=India

Lists

Useful for queues and event processing.

Sets

Store unique values.

Sorted Sets

Ideal for rankings and leaderboards.


Designing Redis Keys

A good naming convention simplifies troubleshooting and cache management.

Recommended format:

<domain>:<entity>:<id>

Examples:

user:profile:1001
product:details:2001
order:summary:5001

Benefits:

  • Easy debugging
  • Logical grouping
  • Easier cache eviction

Time To Live (TTL)

Caches should not live forever.

Example:

SET product:1001 {...} EX 300

The cached data automatically expires after 300 seconds.

Common approaches:

StrategyDescription
Fixed TTLExpire after a specific duration
Sliding TTLExtend expiration on access
Manual EvictionExplicitly remove data

Cache Aside Pattern (Most Popular)

The Cache Aside pattern is the most commonly used Redis caching strategy.

Flow:

Client
   |
Controller
   |
Service
   |
Check Redis
   |
Cache Hit ?
  /      \
Yes       No
 |         |
Return    Repository
 Data        |
             |
          Database
             |
        Store in Redis
             |
          Return

Implementation:

@Service
public class ProductService {

    @Autowired
    private ProductRepository repository;

    @Autowired
    private RedisTemplate<String, Product> redisTemplate;

    public Product getProduct(String productId) {

        String key = "product:" + productId;

        Product product =
                redisTemplate.opsForValue().get(key);

        if(product == null) {

            product = repository
                    .findById(productId)
                    .orElse(null);

            if(product != null) {

                redisTemplate.opsForValue()
                        .set(
                            key,
                            product,
                            Duration.ofMinutes(5)
                        );
            }
        }

        return product;
    }
}

Advantages:

  • Simple implementation
  • Reduces database traffic
  • Easy maintenance

Using Spring Cache Annotations

Spring Boot provides caching abstractions out of the box.

Enable caching:

@EnableCaching
@SpringBootApplication
public class Application {
}

@Cacheable

@Cacheable(
        value = "products",
        key = "#productId")
public Product getProduct(String productId) {

    return repository
            .findById(productId)
            .orElse(null);
}

If data exists in cache, Spring returns it directly.


@CacheEvict

@CacheEvict(
        value = "products",
        key = "#product.id")
public void updateProduct(Product product) {

    repository.save(product);
}

Removes stale cache after updates.


@CachePut

@CachePut(
        value = "products",
        key = "#product.id")
public Product save(Product product) {

    return repository.save(product);
}

Updates cache immediately after saving.


Cache Invalidation

One of the most important aspects of caching is ensuring stale data is not served.

TTL Based Invalidation

Allow cache entries to expire automatically.

Example:

Product Data = 5 Minutes
Customer Profile = 30 Minutes
Reference Data = 24 Hours

Event Based Invalidation

Whenever data changes, remove cache immediately.

public Product updateProduct(Product product) {

    Product updated =
            repository.save(product);

    redisTemplate.delete(
            "product:" + product.getId());

    return updated;
}

The next request reloads fresh data from the database.


Understanding Cache Stampede

The Problem

Assume a popular product is requested thousands of times every minute.

product:1001

Its cache expires at exactly 10:00 AM.

Immediately after expiration:

5000 Requests
      |
      |
Cache Miss
      |
      |
5000 Database Calls

The database suddenly receives thousands of requests.

This phenomenon is called a Cache Stampede.

It can lead to:

  • Database overload
  • Increased response times
  • Service outages

Preventing Cache Stampede Using Distributed Locks

The goal is to allow only one thread to rebuild the cache.

5000 Requests
      |
Cache Miss
      |
Acquire Lock
      |
One Thread Queries Database
      |
Populate Cache
      |
Release Lock
      |
Remaining Threads Read Cache

Implementation:

public Product getProduct(String productId) {

    String cacheKey =
            "product:" + productId;

    Product product =
            redisTemplate.opsForValue()
                         .get(cacheKey);

    if(product != null) {
        return product;
    }

    String lockKey =
            "lock:" + productId;

    Boolean lockAcquired =
            redisTemplate.opsForValue()
                .setIfAbsent(
                    lockKey,
                    "LOCKED",
                    Duration.ofSeconds(10));

    if(Boolean.TRUE.equals(lockAcquired)) {

        try {

            product = repository
                    .findById(productId)
                    .orElse(null);

            if(product != null) {

                redisTemplate.opsForValue()
                        .set(
                            cacheKey,
                            product,
                            Duration.ofMinutes(5));
            }

            return product;

        } finally {

            redisTemplate.delete(lockKey);
        }

    } else {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return redisTemplate.opsForValue()
                .get(cacheKey);
    }
}

Benefits:

  • Only one database call
  • Prevents database spikes
  • Protects backend systems

Randomized TTL Strategy

Another useful technique is adding randomness to expiration times.

Bad:

TTL = 300 Seconds

All records expire simultaneously.

Good:

TTL = 300 + Random(60)

Example:

int ttl =
    300 +
    ThreadLocalRandom.current()
        .nextInt(60);

redisTemplate.opsForValue()
    .set(
        key,
        value,
        Duration.ofSeconds(ttl));

Benefits:

  • Spreads cache expirations
  • Reduces simultaneous cache misses
  • Prevents traffic spikes

Monitoring Redis

Track the following metrics:

MetricTarget
Cache Hit Ratio> 80%
Memory UsageMonitor Continuously
EvictionsMinimal
Latency< 5 ms
Active ConnectionsCapacity Based

Production Architecture

A typical Spring Boot deployment may look like:

Client
   |
Load Balancer
   |
Spring Boot Pods
   |
Redis Cluster
   |
Repository Layer
   |
Database

All application instances share the same Redis cluster, ensuring cache consistency across pods.


Best Practices

  1. Use Cache Aside pattern.
  2. Use meaningful key names.
  3. Apply TTL to all cache entries.
  4. Use Redis Cluster in production.
  5. Monitor cache hit ratio.
  6. Avoid storing large objects.
  7. Implement cache invalidation.
  8. Protect against Cache Stampede.

Conclusion

Redis is one of the most effective ways to improve application performance in Spring Boot microservices. By introducing Redis between the Service and Repository layers, applications can significantly reduce database traffic, improve response times, and scale efficiently.

When combined with proper TTL strategies, cache invalidation mechanisms, and stampede prevention techniques, Redis becomes a powerful foundation for building resilient and high-performance distributed systems.

Leave a Reply

Your email address will not be published. Required fields are marked *