Course topics

By WebNest Studio

Spring Boot Tutorial

Redis with Spring Boot

Redis is an in-memory data store used in almost every large web system. It is extremely fast and supports rich data structures — strings, hashes, lists, sets, sorted sets, streams — which makes it useful far beyond caching: rate limiting, leaderboards, distributed locks, session storage, queues and real-time pub/sub.

This lesson connects Spring Boot to Redis with Spring Data Redis, uses StringRedisTemplate and RedisTemplate for data structures, configures Redis as a distributed cache with per-cache TTLs, stores HTTP sessions in Redis, implements a rate limiter and a leaderboard, and uses pub/sub messaging.

Setup

Add spring-boot-starter-data-redis, which uses the Lettuce client. Configure spring.data.redis.host, port, password (or url) and SSL for managed services such as AWS ElastiCache, Azure Cache or Redis Cloud. With Docker Compose or Testcontainers, a redis service is wired automatically through service connections. Spring Boot auto-configures RedisConnectionFactory, StringRedisTemplate and RedisTemplate.

Templates and Serialization

StringRedisTemplate stores keys and values as plain strings — readable with redis-cli and a good default. RedisTemplate<String, Object> can store objects, but its default JDK serialization produces unreadable binary data tied to Java class versions; configure a JSON serializer instead. opsForValue(), opsForHash(), opsForList(), opsForSet() and opsForZSet() map to Redis commands.

Redis as a Distributed Cache

With the Redis starter on the classpath and @EnableCaching, spring.cache.type=redis makes Spring's cache annotations use Redis. Set a default spring.cache.redis.time-to-live and use a RedisCacheManagerBuilderCustomizer for per-cache TTLs and JSON serialization. All application instances now share one cache, and it survives restarts.

Common Patterns

Redis primitives make several distributed patterns simple:

  • Rate limiting — INCR a per-user key with an expiry per time window.
  • Leaderboards — sorted sets (ZINCRBY, ZREVRANGE) keep scores ordered.
  • Sessions — Spring Session stores HttpSession data in Redis so any instance can serve any user.
  • Distributed locks — SET key value NX PX; for production use a library such as ShedLock or Redisson.
  • Pub/sub and streams — lightweight messaging between instances.

Operational Notes

Redis keeps data in memory: set maxmemory and an eviction policy, always put expiries on cache-like keys, and use key prefixes (webnest:ratelimit:user:42) to keep data organised. Avoid the KEYS command in production — it blocks the server; use SCAN.

Examples

Setup with Docker Compose and properties

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

# compose.yaml
services:
  redis:
    image: redis:8
    ports:
      - "6379"

# application-prod.yml
spring:
  data:
    redis:
      host: ${REDIS_HOST}
      port: 6379
      password: ${REDIS_PASSWORD}
      ssl:
        enabled: true
      timeout: 2s
Output
(dev) Container webnest-redis-1 Started — connection details applied automatically
redis-cli PING -> PONG

Rate limiting and a leaderboard with StringRedisTemplate

Java
@Service
public class RedisPatterns {

    private final StringRedisTemplate redis;

    public RedisPatterns(StringRedisTemplate redis) {
        this.redis = redis;
    }

    /** Allow at most 'limit' requests per user per minute. */
    public boolean allow(String userId, int limit) {
        String key = "webnest:ratelimit:" + userId + ":" + (System.currentTimeMillis() / 60_000);
        Long count = redis.opsForValue().increment(key);
        if (count != null && count == 1) {
            redis.expire(key, Duration.ofMinutes(1));
        }
        return count != null && count <= limit;
    }

    public void addPoints(String student, int points) {
        redis.opsForZSet().incrementScore("webnest:leaderboard", student, points);
    }

    public List<String> top(int n) {
        Set<ZSetOperations.TypedTuple<String>> top =
            redis.opsForZSet().reverseRangeWithScores("webnest:leaderboard", 0, n - 1);
        return top.stream().map(t -> t.getValue() + "=" + t.getScore().intValue()).toList();
    }
}
Output
allow("42", 3) x5 -> true, true, true, false, false
addPoints("asha", 120); addPoints("ravi", 95); addPoints("asha", 30)
top(2) -> [asha=150, ravi=95]

redis-cli> ZREVRANGE webnest:leaderboard 0 -1 WITHSCORES
1) "asha" 2) "150" 3) "ravi" 4) "95"

Redis cache with JSON values and per-cache TTLs

Java
# application.yml
spring:
  cache:
    type: redis
    redis:
      time-to-live: 10m
      key-prefix: "webnest:cache:"

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    RedisCacheManagerBuilderCustomizer redisCaches() {
        RedisCacheConfiguration json = RedisCacheConfiguration.defaultCacheConfig()
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(RedisSerializer.json()));
        return builder -> builder
            .withCacheConfiguration("courseBySlug", json.entryTtl(Duration.ofHours(1)))
            .withCacheConfiguration("exchangeRates", json.entryTtl(Duration.ofMinutes(5)));
    }
}
Output
redis-cli> KEYS webnest:cache:*        (fine locally; use SCAN in production)
1) "webnest:cache:courseBySlug::spring-boot"
redis-cli> TTL "webnest:cache:courseBySlug::spring-boot"
(integer) 3587
redis-cli> GET "webnest:cache:courseBySlug::spring-boot"
"{\"@class\":\"com.webnest.CourseDto\",\"slug\":\"spring-boot\",\"title\":\"Spring Boot\"}"

Shared HTTP sessions and pub/sub between instances

Java
<!-- Spring Session: store HttpSession in Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-session-data-redis</artifactId>
</dependency>

# application.yml
spring:
  session:
    timeout: 30m

// Pub/sub: notify all instances when a course is published
@Configuration
public class PubSubConfig {

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory cf, CoursePublishedListener listener) {
        RedisMessageListenerContainer c = new RedisMessageListenerContainer();
        c.setConnectionFactory(cf);
        c.addMessageListener(listener, new ChannelTopic("webnest:course-published"));
        return c;
    }
}

@Component
public class CoursePublishedListener implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        System.out.println("Course published: " + new String(message.getBody()));
    }
}

// publisher
redis.convertAndSend("webnest:course-published", "spring-ai");
Output
Instance A publishes "spring-ai"
Instance A: Course published: spring-ai
Instance B: Course published: spring-ai
(User sessions survive restarts and work on any instance behind the load balancer.)

Common Mistakes

  • Using RedisTemplate with default JDK serialization, producing unreadable values that break when classes change.
  • Storing cache-like keys without TTLs, slowly filling Redis memory.
  • Running KEYS * in production, which blocks Redis for all clients.
  • Treating Redis as the only copy of important data without persistence or replication configured.
  • Building a home-grown distributed lock without expiry, leaving locks held forever after a crash.

Key Points to Remember

  • spring-boot-starter-data-redis auto-configures Lettuce, StringRedisTemplate and RedisTemplate.
  • Use opsForValue/Hash/List/Set/ZSet for Redis data structures; prefer string or JSON serialization.
  • spring.cache.type=redis turns Redis into a shared cache with configurable TTLs.
  • Redis enables rate limiting, leaderboards, shared sessions, locks and pub/sub.
  • Always set expiries, key prefixes and memory limits; use SCAN instead of KEYS.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.