Why Simple Spring Boot APIs Slow Down Under Load and How Proper Redis Caching Fixes It

The article walks through a step‑by‑step integration of Redis caching into a Spring Boot application, showing how to add dependencies, configure connections, enable caching annotations, customize serialization, simulate slow data sources, and fine‑tune TTLs to turn laggy endpoints into smooth, high‑throughput services.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Why Simple Spring Boot APIs Slow Down Under Load and How Proper Redis Caching Fixes It

Problem

Concurrent requests to a simple Spring Boot endpoint can cause response times to jump from tens of milliseconds to several seconds. The latency pattern (first request slow, subsequent requests fast) indicates a missing cache rather than a database bottleneck.

Dependencies

<dependencies>
    <!-- Web layer support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Cache abstraction -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <!-- Redis integration -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Redis core -->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
    </dependency>
</dependencies>

Redis connection configuration

spring.application.name=redis-demo
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.database=0
# In production you would also set password, SSL, and pool parameters

Enable caching

package com.icoderoad.redisdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class RedisDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisDemoApplication.class, args);
    }
}

Cache serialization (JSON)

Replace the default JDK serializer with a JSON serializer to improve readability and performance.

package com.icoderoad.redisdemo.config;

import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

@Configuration
public class CacheConfig {
    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10)) // unified TTL
            .disableCachingNullValues()
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new GenericJackson2JsonRedisSerializer()));
    }
}

The configuration performs three key actions:

Sets a uniform time‑to‑live (TTL) of 10 minutes for cached entries.

Disables caching of null values to avoid cache pollution.

Uses GenericJackson2JsonRedisSerializer for JSON serialization.

Simulated slow data source

Define a Product entity and a repository that deliberately sleeps for 2 seconds to emulate a slow query.

// src/main/java/com/icoderoad/redisdemo/product/Product.java
package com.icoderoad.redisdemo.product;

public class Product {
    private Long id;
    private String name;
    private double price;
    // getters and setters omitted
}
// src/main/java/com/icoderoad/redisdemo/product/ProductRepository.java
package com.icoderoad.redisdemo.product;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;

@Repository
public class ProductRepository {
    private final Map<Long, Product> store = new ConcurrentHashMap<>();

    public ProductRepository() {
        store.put(1L, new Product(1L, "Laptop", 80000));
        store.put(2L, new Product(2L, "Phone", 40000));
    }

    public Product findById(Long id) {
        simulateSlowCall(); // Thread.sleep(2000)
        return store.get(id);
    }

    public Product save(Product product) {
        store.put(product.getId(), product);
        return product;
    }

    public void deleteById(Long id) {
        store.remove(id);
    }

    private void simulateSlowCall() {
        try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

The first request incurs the 2‑second delay; subsequent requests return almost instantly because the result is cached.

Service layer with Spring Cache annotations

// src/main/java/com/icoderoad/redisdemo/product/ProductService.java
package com.icoderoad.redisdemo.product;

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    private final ProductRepository repository;
    public ProductService(ProductRepository repository) { this.repository = repository; }

    @Cacheable(cacheNames = "products", key = "#id")
    public Product getProduct(Long id) {
        System.out.println("Loading product from repository...");
        return repository.findById(id);
    }

    @CachePut(cacheNames = "products", key = "#result.id")
    public Product updateProduct(Product product) {
        System.out.println("Updating product and cache...");
        return repository.save(product);
    }

    @CacheEvict(cacheNames = "products", key = "#id")
    public void deleteProduct(Long id) {
        System.out.println("Removing product and cache entry...");
        repository.deleteById(id);
    }
}

Annotation effects: @Cacheable – checks the cache first; method executes only on a miss. @CachePut – always executes the method and updates the cache with the result. @CacheEvict – removes the specified entry from the cache.

Controller layer

// src/main/java/com/icoderoad/redisdemo/product/ProductController.java
package com.icoderoad.redisdemo.product;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/products")
public class ProductController {
    private final ProductService service;
    public ProductController(ProductService service) { this.service = service; }

    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) { return service.getProduct(id); }

    @PutMapping("/{id}")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
        product.setId(id);
        return service.updateProduct(product);
    }

    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) { service.deleteProduct(id); }
}

Fine‑grained TTL tuning

Different caches can have distinct TTLs by customizing the RedisCacheManager.

// src/main/java/com/icoderoad/redisdemo/config/CacheTuningConfig.java
package com.icoderoad.redisdemo.config;

import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder;
import org.springframework.data.redis.cache.RedisCacheManagerBuilderCustomizer;

@Configuration
public class CacheTuningConfig {
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return (RedisCacheManagerBuilder builder) -> builder
            .withCacheConfiguration("products",
                RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)))
            .withCacheConfiguration("users",
                RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(1)));
    }
}

Example rationale: product data changes slowly, so a 5‑minute TTL is acceptable; user profile data changes frequently, so a 1‑minute TTL reduces staleness.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRedisCachingSpring BootTTLannotationsspring-cache
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.