Stop Just Using Redis: A Complete Spring Boot + Redis + Docker Caching Guide

The article explains why naïve reliance on databases causes performance bottlenecks, introduces cache fundamentals, details Redis’s strengths, and walks through a full Spring Boot‑Redis integration deployed with Docker, including configuration, annotations, and practical API testing.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Stop Just Using Redis: A Complete Spring Boot + Redis + Docker Caching Guide

Cache fundamentals

A cache is a small, fast, short‑lived key‑value store whose sole purpose is to trade space for time, reducing accesses to the slowest resource—typically the database. Frequently accessed, rarely changed data (product details, system configuration, dictionaries, user basics) cause database connection saturation, high response times, and reduced throughput when repeatedly queried.

Cache workflow

Request arrives.

Check the cache first.

If data exists, return it (Cache Hit).

If not, query the database, write the result into the cache, then return it (Cache Miss).

Why Redis is the de‑facto standard

Redis (Remote Dictionary Server) is an in‑memory key‑value store that supports multiple data structures (String, Hash, List, Set, ZSet) and offers high performance with low latency. These characteristics make it suitable for caching, session management, leaderboards, and distributed locks.

Integrating Redis with Spring Boot

Add the starter dependency:

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

The starter automatically configures Redis connection handling, RedisTemplate, cache abstraction, and a default serialization mechanism.

Running Redis with Docker

services:
  redis:
    image: redis:7.4.2
    container_name: redis-cache
    ports:
      - "6379:6379"

Start with docker compose up -d.

Spring Boot configuration

Configure the data source and cache type in application.yml:

spring:
  application:
    name: spring-boot-redis-cache
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: password
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379

Enable caching in the main class:

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

Redis cache configuration

package com.icoderoad.redis.config;

import com.icoderoad.redis.dto.ProductDto;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;

@Configuration
public class RedisConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues()
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                new Jackson2JsonRedisSerializer<>(ProductDto.class)));
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

This configuration sets a TTL of 10 minutes, disables caching of null values, and uses JSON serialization for readability and compatibility.

Service layer with cache annotations

package com.icoderoad.redis.service;

import com.icoderoad.redis.dto.ProductDto;
import com.icoderoad.redis.entity.Product;
import com.icoderoad.redis.repository.ProductRepository;
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 static final String CACHE_NAME = "PRODUCT_CACHE";
    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    @CachePut(value = CACHE_NAME, key = "#result.id")
    public ProductDto create(ProductDto dto) {
        Product product = new Product(dto.name(), dto.price());
        Product saved = repository.save(product);
        return new ProductDto(saved.getId(), saved.getName(), saved.getPrice());
    }

    @Cacheable(value = CACHE_NAME, key = "#id")
    public ProductDto findById(Long id) {
        Product product = repository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Product not found: " + id));
        return new ProductDto(product.getId(), product.getName(), product.getPrice());
    }

    @CachePut(value = CACHE_NAME, key = "#result.id")
    public ProductDto update(ProductDto dto) {
        Product product = repository.findById(dto.id())
                .orElseThrow(() -> new IllegalArgumentException("Product not found"));
        product.setName(dto.name());
        product.setPrice(dto.price());
        Product updated = repository.save(product);
        return new ProductDto(updated.getId(), updated.getName(), updated.getPrice());
    }

    @CacheEvict(value = CACHE_NAME, key = "#id")
    public void delete(Long id) {
        repository.deleteById(id);
    }
}

Cache annotation summary

@Cacheable

: checks the cache first; method executes only on a miss. @CachePut: forces the method result to be written into the cache after execution. @CacheEvict: removes the specified entry from the cache.

Direct CacheManager usage

Cache cache = cacheManager.getCache("PRODUCT_CACHE");
cache.put(productId, product);

Using CacheManager provides more flexibility, while annotations give concise syntax.

API testing

POST   http://localhost:8080/api/product
GET    http://localhost:8080/api/product/{id}
PUT    http://localhost:8080/api/product
DELETE http://localhost:8080/api/product/{id}

The first GET request queries the database; subsequent GET requests retrieve the data directly from Redis, demonstrating a cache hit.

Result

Introducing Redis as a cache reduces database pressure dramatically, improves response times, and enables the system to serve many more users without additional hardware.

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.

JavaDockerBackend DevelopmentRedisCachingSpring BootCacheable
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.