Lab: Redis Caching with Spring Boot


Lab Overview: Redis Caching with Spring Boot

This lab will walk you through creating a simple Spring Boot application that uses Redis for caching. You will implement a book catalog service where Redis caching will significantly improve data retrieval performance.

Learning Objectives

By the end of this lab, you will:

  • Understand the fundamentals of caching and why it’s important
  • Set up a Redis server and connect to it from a Spring Boot application
  • Implement Spring’s caching abstraction with Redis as the cache provider
  • Measure and observe the performance benefits of caching

Lab Instructions

Part 1: Setting up the Environment

  1. Prerequisites:
  • Java 17 or higher installed
  • Maven or Gradle
  • IDE (IntelliJ IDEA, Eclipse, or VS Code)
  • Docker (for running Redis)
  1. Start Redis using Docker: Run the following command in your terminal:
docker run --name redis -p 6379:6379 -d redis

This starts a Redis server on the default port 6379.

  1. Create a new Spring Boot project:
  • Visit Spring Initializr
  • Select:
    • Maven or Gradle project
    • Java 17
    • Spring Boot 3.x
    • Dependencies: Spring Web, Spring Data Redis, Lombok (optional)
  • Generate and unzip the project

Part 2: Creating the Application Structure

  1. Create a Book entity:
package com.example.redislab.model;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;
    private String title;
    private String author;
    private String isbn;
    private double price;

    // This toString helps us see when a Book is retrieved from cache vs database
    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", author='" + author + '\'' +
                ", isbn='" + isbn + '\'' +
                ", price=" + price +
                '}';
    }
}
  1. Create a Repository Interface (simulating a database):
package com.example.redislab.repository;

import com.example.redislab.model.Book;
import org.springframework.stereotype.Repository;

import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@Repository
public class BookRepository {

    private final Map<Long, Book> bookStore = new HashMap<>();

    @PostConstruct
    public void init() {
        // Populate with sample data
        bookStore.put(1L, new Book(1L, "To Kill a Mockingbird", "Harper Lee", "978-0061120084", 14.99));
        bookStore.put(2L, new Book(2L, "1984", "George Orwell", "978-0451524935", 12.99));
        bookStore.put(3L, new Book(3L, "The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", 11.99));
        bookStore.put(4L, new Book(4L, "Pride and Prejudice", "Jane Austen", "978-0141439518", 9.99));
        bookStore.put(5L, new Book(5L, "The Catcher in the Rye", "J.D. Salinger", "978-0316769488", 10.99));
    }

    public Book findById(Long id) {
        // Simulate database latency
        try {
            Thread.sleep(1000); // 1 second delay
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Fetching book with ID: " + id + " from database");
        return bookStore.get(id);
    }

    public Book save(Book book) {
        // Simulate database latency
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        bookStore.put(book.getId(), book);
        return book;
    }

    public void delete(Long id) {
        bookStore.remove(id);
    }
}
  1. Create a Service Layer:
package com.example.redislab.service;

import com.example.redislab.model.Book;
import com.example.redislab.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
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 BookService {

    private final BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    // Add caching annotation - books will be cached by id
    @Cacheable(value = "books", key = "#id")
    public Book findById(Long id) {
        return bookRepository.findById(id);
    }

    // Update cache when a book is updated
    @CachePut(value = "books", key = "#book.id")
    public Book updateBook(Book book) {
        return bookRepository.save(book);
    }

    // Remove from cache when a book is deleted
    @CacheEvict(value = "books", key = "#id")
    public void deleteBook(Long id) {
        bookRepository.delete(id);
    }

    // Clear entire cache
    @CacheEvict(value = "books", allEntries = true)
    public void clearAllCaches() {
        System.out.println("Clearing all books from cache");
    }
}
  1. Create a Controller:
package com.example.redislab.controller;

import com.example.redislab.model.Book;
import com.example.redislab.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping("/{id}")
    public Book getBook(@PathVariable Long id) {
        long startTime = System.currentTimeMillis();
        Book book = bookService.findById(id);
        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime) + " ms");
        return book;
    }

    @PutMapping("/{id}")
    public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
        book.setId(id);
        return bookService.updateBook(book);
    }

    @DeleteMapping("/{id}")
    public void deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
    }

    @DeleteMapping("/cache")
    public void clearCache() {
        bookService.clearAllCaches();
    }
}

Part 3: Configure Redis and Spring Caching

  1. Update application.properties:
# Redis Configuration
spring.data.redis.host=localhost
spring.data.redis.port=6379

# Logging
logging.level.org.springframework.cache=TRACE
  1. Create Redis Configuration Class:
package com.example.redislab.config;

import org.springframework.cache.annotation.EnableCaching;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // Create a default cache configuration that expires after 10 minutes
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))  // Set Time-To-Live to 10 minutes
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                new GenericJackson2JsonRedisSerializer()));  // Use JSON serialization

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(cacheConfig)
                .build();
    }
}
  1. Create application main class:
package com.example.redislab;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

Part 4: Running and Testing the Application

  1. Start the Spring Boot application:
./mvnw spring-boot:run

Or using Gradle:

./gradlew bootRun
  1. Test the endpoints:

Using a tool like curl, Postman, or your web browser, make the following requests:

  • First request to get a book:

    GET http://localhost:8080/api/books/1
  • Second request for the same book (should be much faster):

    GET http://localhost:8080/api/books/1
  • Update a book:

    PUT http://localhost:8080/api/books/1
    Body: {"title":"To Kill a Mockingbird - Updated Edition","author":"Harper Lee","isbn":"978-0061120084","price":19.99}
  • Delete a book from cache:

    DELETE http://localhost:8080/api/books/1
  • Clear the entire cache:

    DELETE http://localhost:8080/api/books/cache

Part 5: Understanding the Results

  1. Observe the console output when making requests:
  • The first time a book is requested, you should see a message about fetching from the database and a request time of around 1000ms.
  • The second time, you should not see the database message, and the request should be much faster (a few milliseconds).
  1. Understand what’s happening:
  • First request: Data is fetched from the “database” (our HashMap with simulated delay), then stored in Redis
  • Subsequent requests: Data is retrieved from Redis cache, bypassing the slow database access
  • After updating a book: Cache is updated with new data
  • After deleting a book: Its entry is removed from the cache
  • After clearing the cache: All books are removed from Redis

Lab Understanding

After completing the lab, explain:

  1. Why is caching important in web applications? What are the tradeoffs?
  2. What problems might arise if we don’t properly manage our cache (e.g., when data is updated)?
  3. How do the different cache annotations work?
  • @Cacheable - Caches the result of the method
  • @CachePut - Updates the cache without affecting method execution
  • @CacheEvict - Removes entries from the cache
  1. What would happen if we didn’t use a serializable class with Redis?
  2. How might we implement more complex caching strategies (different TTLs for different types of data, etc.)?

Extensions

Additional challenges:

  1. Implement a cache hit counter to measure cache effectiveness
  2. Add a method to batch-load books and observe caching behavior with high volumes
  3. Configure different expiration times for different types of data
  4. Implement conditional caching (only cache certain books based on criteria)
  5. Create a simple front-end that demonstrates the speed difference in real-time

This lab provides a hands-on introduction to Redis caching with Spring Boot. The simulated database delay makes the benefits of caching immediately apparent, which helps reinforce why caching is valuable in real-world applications.


By Wahid Hamdi