How to Use Redis in Your Spring Boot Project: Step-by-Step Tutorial
This chapter provides a step-by-step guide to integrating Redis into a Spring Boot application. We explore why high-throughput systems require in-memory caching, contrast local JVM caching with distributed caching, address common serialization pitfalls with Spring Boot's RedisTemplate, and build a decoupled, production-grade generic caching service to reduce latency and external API costs.
1. The Caching Imperative: Why Do We Need Redis?
When building scalable web applications, handling repetitive database queries and expensive external API calls becomes a major bottleneck.
Real-Life Analogy: The Subscription Plans & Weather API Scenario
Imagine an application where 10 million () users access the subscription plans page (/plans) or check the current weather in a major city like Mumbai within a single minute.
If the server executes a heavy method (getPlans() or getWeather()) for every single request:
- Resource Waste: The database or external API repeatedly computes the exact same result for unauthenticated or region-identical users.
- High Latency: Fetching data from a disk-bound database or across the network introduces tens to hundreds of milliseconds of delay. In modern applications, even a 1-millisecond delay can cause user churn.
- Monetary Cost: External APIs often charge per request. Millions of redundant calls drastically increase operational costs.
The Solution: Place an In-Memory Cache (Redis) in front of your database or external APIs. The first request fetches the data and saves it in Redis; all subsequent requests within a specified window are served directly from fast memory.
2. What is Redis? In-Memory vs. Disk Storage
Redis (Remote Dictionary Server) is an open-source, in-memory data structure store used primarily as a cache, database, streaming engine, and message broker.
Key Differences: RAM vs. Disk
- Access Speeds: Data stored in RAM (Main Memory) is accessed in nanoseconds, whereas data stored on Disk (SSD/HDD) is accessed in milliseconds.
- Volatilization: RAM is volatile storage designed for high-speed read/write access, making it ideal for caching temporary data.
3. Local JVM Heap Caching vs. Remote Redis Caching
A common question is: Why use Redis when we can store a HashMap in the JVM memory using Java?
// Naive Local Caching inside Spring Boot JVM
private Map<String, Object> localCache = new ConcurrentHashMap<>();
While local JVM caching is simple, it faces critical limitations in enterprise systems:
4. Setting Up Redis locally via WSL (Windows Subsystem for Linux)
Redis is not natively supported on Windows. To run Redis locally on Windows for development, use Windows Subsystem for Linux (WSL).
Step 1: Install WSL
Open Windows PowerShell or Command Prompt as Administrator:
wsl --install
Note: Restart your machine after installation if prompted.
Step 2: Install Redis inside WSL
Open your WSL terminal and run the following commands:
# Update Ubuntu package lists
sudo apt update
# Install Redis Server
sudo apt install redis-server -y
# Start the Redis service
sudo service redis-server start
Step 3: Verify Installation with redis-cli
Launch the Redis Command-Line Interface:
redis-cli
Test the server health and basic key-value operations:
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET name "Akshit"
OK
127.0.0.1:6379> GET name
"Akshit"
127.0.0.1:6379> EXIT
5. Integrating Redis into Spring Boot
Step 1: Add Dependency (pom.xml)
Add the spring-boot-starter-data-redis starter to your Spring Boot project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Step 2: Configure Application Properties (application.yml)
Define the host and port parameters for your Redis server:
spring:
data:
redis:
host: localhost
port: 6379
Note: If using an older Spring Boot version, the key path might be
spring.redis.hostandspring.redis.port.
6. The Naive RedisTemplate & Serialization Pitfall
Spring Boot auto-configures a basic RedisTemplate bean once the starter dependency is present. However, using it out-of-the-box leads to serialization conflicts.
Naive Code Example
@SpringBootTest
public class RedisTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void testRedis() {
// Storing data via Spring Boot
redisTemplate.opsForValue().set("email", "user@example.com");
// Reading data back via Spring Boot
Object email = redisTemplate.opsForValue().get("email");
System.out.println(email); // Output: user@example.com
}
}
The Production Bug: Serializer Mismatch
If you set a key via redis-cli (e.g., SET salary 10000) and try to read it using Spring Boot's auto-configured RedisTemplate, it returns null. Conversely, keys written by Spring Boot look garbled inside redis-cli:
127.0.0.1:6379> KEYS *
1) "\xac\xed\x00\x05t\x00\x05email" # Binary JDK Serialization Garbage!
- Root Cause: By default,
RedisTemplateusesJdkSerializationRedisSerializer, converting keys and values into binary stream representations rather than plain strings.
7. Configuring a Production-Grade RedisConfig Bean
To ensure compatibility between Spring Boot, redis-cli, and other microservices, construct a explicit @Configuration class to enforce StringRedisSerializer.
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// Assign the underlying connection factory (Lettuce/Jedis)
redisTemplate.setConnectionFactory(connectionFactory);
// Configure explicit String Serializers for Keys and Values
StringRedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
return redisTemplate;
}
}
8. Building a Generic Caching Component (RedisService)
Instead of polluting domain services with raw Redis operations, decouple caching logic into a dedicated, generic service (RedisService) using Jackson's ObjectMapper.
The ClassCastException Problem
When saving complex Java Objects (like WeatherResponse POJOs), StringRedisSerializer expects a String input. Passing a raw Object directly results in a ClassCastException. To solve this, convert Java Objects to JSON Strings before saving, and parse them back to Java Objects upon retrieval.
package com.example.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Generic Get Method to retrieve cached data and map it to a target POJO Class.
*/
public <T> T get(String key, Class<T> entityClass) {
try {
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
// Deserialize JSON string back to target Entity Class
return objectMapper.readValue(value.toString(), entityClass);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Generic Set Method to convert Java POJO to JSON String and store with a TTL.
*/
public void set(String key, Object object, Long ttlInSeconds) {
try {
// Serialize Java Object to JSON String
String jsonValue = objectMapper.writeValueAsString(object);
if (ttlInSeconds != null && ttlInSeconds > 0) {
redisTemplate.opsForValue().set(key, jsonValue, ttlInSeconds, TimeUnit.SECONDS);
} else {
// Persistent storage without expiration
redisTemplate.opsForValue().set(key, jsonValue);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
9. Practical Integration: Caching External Weather API Responses
Here is an end-to-end practical implementation where WeatherService uses RedisService to cache response payloads for 300 seconds (5 minutes).
package com.example.service;
import com.example.api.response.WeatherResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class WeatherService {
@Autowired
private RedisService redisService;
@Autowired
private RestTemplate restTemplate;
private static final String API_KEY = "YOUR_WEATHER_API_KEY";
private static final String API_URL = "https://api.weatherapi.com/v1/current.json?key=" + API_KEY + "&q=";
public WeatherResponse getWeather(String city) {
String cacheKey = "weather:" + city.toLowerCase();
// Step 1: Check Redis Cache (Cache Hit Scenario)
WeatherResponse cachedResponse = redisService.get(cacheKey, WeatherResponse.class);
if (cachedResponse != null) {
return cachedResponse; // Fast Nanosecond Return!
}
// Step 2: Cache Miss -> Call Third-Party Weather API
String url = API_URL + city;
ResponseEntity<WeatherResponse> response = restTemplate.getForEntity(url, WeatherResponse.class);
WeatherResponse body = response.getBody();
// Step 3: Populate Redis Cache with a 300-second TTL
if (body != null) {
redisService.set(cacheKey, body, 300L);
}
return body;
}
}
10. End-to-End Request Flow
The diagram below illustrates the exact execution path when a user requests weather information:
11. Redis Operations & Lifetime Management Reference
Redis TTL Semantics
Redis CLI vs Spring Boot Method Comparison
12. Summary and Key Takeaways
- In-Memory Performance: Redis operates in RAM, offering nanosecond read/write operations compared to millisecond disk database queries.
- Cost & Latency Reduction: Caching frequently accessed, non-changing responses prevents server overload and avoids unnecessary third-party API costs.
- Distributed Consistency: Unlike local JVM
HashMapcaches, a centralized Redis server shares cached state across all running application instances. - WSL Development: On Windows environments, run Redis using Ubuntu inside WSL via
sudo service redis-server start. - Explicit Serialization: Always override the default
RedisTemplatebean in Spring Boot usingStringRedisSerializerfor both keys and values to avoid garbled binary data (\xac\xed...). - Decoupled Architecture: Encapsulate generic JSON conversion logic using Jackson's
ObjectMapperinside a helper service likeRedisService. - Time-To-Live (TTL): Always configure appropriate expiration windows (TTL) to ensure cache freshness and avoid memory leaks.