How to Integrate Redis Cache into Spring Boot: Step‑by‑Step Guide

This article explains why Redis is essential for Spring Boot projects, walks through adding dependencies, configuring connection and serialization, demonstrates CRUD operations for all Redis data types, shows @Cacheable usage, and lists common pitfalls with practical solutions.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
How to Integrate Redis Cache into Spring Boot: Step‑by‑Step Guide

Why use Redis as cache

Reduce database pressure : cache high‑frequency data (e.g., user info, product lists) to avoid repeated DB queries and improve response time from milliseconds to microseconds.

Support multiple scenarios : beyond caching, Redis provides distributed locks, counters, rate limiting, simple message queues, and leaderboards.

High performance and high availability : in‑memory operations are extremely fast; Redis supports master‑slave replication and Sentinel for HA.

Step 1 – Add Maven dependencies

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

<!-- Connection pool -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>

<!-- Lombok (optional) -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>

The starter bundles the Redis client (Lettuce by default) and works with commons-pool2 for better performance.

Step 2 – Core application.yml configuration

spring:
  redis:
    host: localhost               # server address
    port: 6379                    # default port
    password: 123456            # comment out if none
    database: 0                  # DB index 0‑15
    timeout: 5000                # ms
    lettuce:
      pool:
        max-active: 100        # core connections, adjust after load test
        max-idle: 20
        min-idle: 5
        max-wait: 1000          # ms
    serialization:
      key-prefix: "springboot:redis:"   # avoid key conflict across projects
      expire-default: 3600    # default TTL in seconds (1 hour)

Step 3 – Redis serialization configuration

Spring Boot’s default serializer may produce unreadable keys/values. The following configuration switches to JSON serialization while keeping key readability.

package com.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
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;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@ConfigurationProperties(prefix = "redis.serialization")
public class RedisConfig {
    private String keyPrefix;          // cache key prefix
    private Long expireDefault;        // default TTL (seconds)

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setValueSerializer(jsonSerializer);
        redisTemplate.setHashValueSerializer(jsonSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(expireDefault))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();
        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        cacheConfigs.put("userCache", config.entryTtl(Duration.ofSeconds(7200)));   // 2 h
        cacheConfigs.put("productCache", config.entryTtl(Duration.ofSeconds(3600))); // 1 h
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .withInitialCacheConfigurations(cacheConfigs)
                .build();
    }

    // getters & setters (Lombok can generate)
    public String getKeyPrefix() { return keyPrefix; }
    public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; }
    public Long getExpireDefault() { return expireDefault; }
    public void setExpireDefault(Long expireDefault) { this.expireDefault = expireDefault; }
}

Redis data‑type operations

String (most common)

Typical scenarios: user info, verification codes, tokens, simple counters.

package com.demo.service;

import com.demo.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedisStringService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisConfig redisConfig;

    private String getKey(String key) {
        return redisConfig.getKeyPrefix() + key;
    }

    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(getKey(key), value);
    }

    public void setWithExpire(String key, Object value, Long expireTime, TimeUnit unit) {
        redisTemplate.opsForValue().set(getKey(key), value, expireTime, unit);
    }

    public Object get(String key) {
        return redisTemplate.opsForValue().get(getKey(key));
    }

    public void update(String key, Object value) {
        set(key, value);
    }

    public Boolean delete(String key) {
        return redisTemplate.delete(getKey(key));
    }

    public Long increment(String key, Long delta) {
        return redisTemplate.opsForValue().increment(getKey(key), delta);
    }

    public Long decrement(String key, Long delta) {
        return redisTemplate.opsForValue().decrement(getKey(key), delta);
    }

    // Example: cache a user object for 2 h
    public void cacheUser(User user) {
        String key = "user:" + user.getId();
        setWithExpire(key, user, 7200L, TimeUnit.SECONDS);
    }

    public User getCachedUser(Long userId) {
        String key = "user:" + userId;
        return (User) get(key);
    }
}

Hash (object‑level caching)

Suitable for storing objects where individual fields may be updated without rewriting the whole object.

package com.demo.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedisHashService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisConfig redisConfig;

    private String getKey(String key) {
        return redisConfig.getKeyPrefix() + key;
    }

    private HashOperations<String, String, Object> getHashOps() {
        return redisTemplate.opsForHash();
    }

    public void putAll(String key, Map<String, Object> map) {
        String redisKey = getKey(key);
        getHashOps().putAll(redisKey, map);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
    }

    public void put(String key, String field, Object value) {
        String redisKey = getKey(key);
        getHashOps().put(redisKey, field, value);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
    }

    public Object get(String key, String field) {
        return getHashOps().get(getKey(key), field);
    }

    public Map<String, Object> getAll(String key) {
        return getHashOps().entries(getKey(key));
    }

    public Set<String> getHashKeys(String key) {
        return getHashOps().keys(getKey(key));
    }

    public void update(String key, String field, Object value) {
        put(key, field, value);
    }

    public Long delete(String key, String... fields) {
        return getHashOps().delete(getKey(key), (Object[]) fields);
    }

    // Example: cache user info as a hash
    public void cacheUserHash(Long userId, Map<String, Object> userMap) {
        String key = "user:hash:" + userId;
        putAll(key, userMap);
    }

    public void updateUserName(Long userId, String newName) {
        String key = "user:hash:" + userId;
        put(key, "username", newName);
    }
}

List (ordered collection)

Useful for queues, recent‑item lists, browsing history, etc.

package com.demo.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedisListService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisConfig redisConfig;

    private String getKey(String key) {
        return redisConfig.getKeyPrefix() + key;
    }

    private ListOperations<String, Object> getListOps() {
        return redisTemplate.opsForList();
    }

    public Long leftPush(String key, Object value) {
        String redisKey = getKey(key);
        Long result = getListOps().leftPush(redisKey, value);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
        return result;
    }

    public Long rightPush(String key, Object value) {
        String redisKey = getKey(key);
        Long result = getListOps().rightPush(redisKey, value);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
        return result;
    }

    public Object leftPop(String key) {
        return getListOps().leftPop(getKey(key));
    }

    public Object rightPop(String key) {
        return getListOps().rightPop(getKey(key));
    }

    public List<Object> range(String key, Long start, Long end) {
        return getListOps().range(getKey(key), start, end);
    }

    public Long size(String key) {
        return getListOps().size(getKey(key));
    }

    public Long remove(String key, Long count, Object value) {
        return getListOps().remove(getKey(key), count, value);
    }

    // Example: keep latest 10 browse records per user
    public void addBrowseRecord(Long userId, String productId) {
        String key = "browse:record:" + userId;
        rightPush(key, productId);
        Long size = size(key);
        if (size > 10) {
            leftPop(key);
        }
    }

    public List<Object> getBrowseRecords(Long userId) {
        String key = "browse:record:" + userId;
        return range(key, 0L, -1L);
    }
}

Set (unordered, deduplication)

Ideal for tag systems, friend lists, and set operations such as intersection and union.

package com.demo.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedisSetService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisConfig redisConfig;

    private String getKey(String key) {
        return redisConfig.getKeyPrefix() + key;
    }

    private SetOperations<String, Object> getSetOps() {
        return redisTemplate.opsForSet();
    }

    public Long add(String key, Object... values) {
        String redisKey = getKey(key);
        Long result = getSetOps().add(redisKey, values);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
        return result;
    }

    public Set<Object> members(String key) {
        return getSetOps().members(getKey(key));
    }

    public Boolean isMember(String key, Object value) {
        return getSetOps().isMember(getKey(key), value);
    }

    public Long remove(String key, Object... values) {
        return getSetOps().remove(getKey(key), values);
    }

    public Long size(String key) {
        return getSetOps().size(getKey(key));
    }

    public Set<Object> intersect(String key1, String key2) {
        return getSetOps().intersect(getKey(key1), getKey(key2));
    }

    public Set<Object> union(String key1, String key2) {
        return getSetOps().union(getKey(key1), getKey(key2));
    }

    // Example: add tags to a user
    public void addUserTag(Long userId, String... tags) {
        String key = "user:tag:" + userId;
        add(key, (Object[]) tags);
    }

    public Set<Object> getUserTags(Long userId) {
        String key = "user:tag:" + userId;
        return members(key);
    }

    public Set<Object> getCommonTags(Long userId1, Long userId2) {
        String key1 = "user:tag:" + userId1;
        String key2 = "user:tag:" + userId2;
        return intersect(key1, key2);
    }
}

ZSet (sorted set – leaderboard)

Each element has a score; useful for ranking, leaderboards, and sorted queries.

package com.demo.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedisZSetService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final RedisConfig redisConfig;

    private String getKey(String key) {
        return redisConfig.getKeyPrefix() + key;
    }

    private ZSetOperations<String, Object> getZSetOps() {
        return redisTemplate.opsForZSet();
    }

    public Boolean add(String key, Object value, Double score) {
        String redisKey = getKey(key);
        Boolean result = getZSetOps().add(redisKey, value, score);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
        return result;
    }

    public Long addBatch(String key, Set<ZSetOperations.TypedTuple<Object>> tuples) {
        String redisKey = getKey(key);
        Long result = getZSetOps().add(redisKey, tuples);
        redisTemplate.expire(redisKey, redisConfig.getExpireDefault(), TimeUnit.SECONDS);
        return result;
    }

    public Double incrementScore(String key, Object value, Double delta) {
        return getZSetOps().incrementScore(getKey(key), value, delta);
    }

    public Set<ZSetOperations.TypedTuple<Object>> rangeByScore(String key, Double min, Double max) {
        return getZSetOps().rangeByScoreWithScores(getKey(key), min, max);
    }

    public Set<ZSetOperations.TypedTuple<Object>> reverseRangeByScore(String key, Double min, Double max, Long start, Long end) {
        return getZSetOps().reverseRangeByScoreWithScores(getKey(key), min, max, start, end);
    }

    public Double score(String key, Object value) {
        return getZSetOps().score(getKey(key), value);
    }

    public Long size(String key) {
        return getZSetOps().size(getKey(key));
    }

    public Long remove(String key, Object... values) {
        return getZSetOps().remove(getKey(key), values);
    }

    // Example: top‑10 product sales leaderboard (descending)
    public Set<ZSetOperations.TypedTuple<Object>> getProductSalesTop10() {
        String key = "product:sales:rank";
        return reverseRangeByScore(key, 0.0, Double.MAX_VALUE, 0L, 9L);
    }

    public void updateProductSales(String productId) {
        String key = "product:sales:rank";
        incrementScore(key, productId, 1.0);
    }
}

Using Spring’s @Cacheable annotation

For simple query‑cache patterns, the annotation replaces manual Redis calls.

package com.demo.service;

import com.demo.entity.User;
import com.demo.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserCacheService {
    private final UserMapper userMapper;

    @Cacheable(value = "userCache", key = "#userId", unless = "#result == null")
    public User getUserById(Long userId) {
        return userMapper.selectById(userId);
    }

    @CachePut(value = "userCache", key = "#user.id")
    public User updateUser(User user) {
        userMapper.updateById(user);
        return user;
    }

    @CacheEvict(value = "userCache", key = "#userId")
    public void deleteUser(Long userId) {
        userMapper.deleteById(userId);
    }
}
Important: add @EnableCaching on the Spring Boot main class to activate the caching infrastructure.

Common pitfalls and mitigation

Cache garbled data : Values cannot be deserialized. Solution : Configure a custom JSON serializer (e.g., Jackson2JsonRedisSerializer) instead of the default JDK serializer.

Cache penetration : Requests for non‑existent DB rows always hit the DB. Solution : Cache null values with a short TTL; optionally use a Bloom filter to pre‑check existence.

Cache breakdown : Hot key expires, massive concurrent DB hits. Solution : Mark hot keys as never‑expire; use a mutex lock so only one thread queries the DB.

Cache avalanche : Many keys expire simultaneously, overwhelming the DB. Solution : Add random jitter to TTL; keep critical data evergreen and refresh asynchronously.

Key naming conflict : Different modules overwrite each other’s keys. Solution : Use a uniform prefix like project:module:entity:id.

Expiration strategy : Too long wastes memory; too short reduces hit rate. Solution : Set short TTL for high‑frequency mutable data, long TTL for stable data.

Redis security : Unauthenticated access leads to data leakage. Solution : Enable a strong password and IP‑based firewall rules.

Connection‑pool exhaustion : High concurrency causes timeout errors. Solution : Adjust max-active, max-idle, min-idle based on load‑test results.

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.

BackendJavaCacheRedisspring-boot
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

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.