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.
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 parametersEnable 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
