Why this matters
- Catalog lookups by ISBN dominate BookStore traffic — caching bestseller lists and book details cuts database load by orders of magnitude.
- A local in-memory cache per JVM goes stale when inventory changes on another node; Redis gives every instance the same view.
@Cacheablekeeps caching logic out of business code — the service method looks like a plain database read until you inspect the annotations.
Cache-aside with ElastiCache
Cache abstraction pieces
@EnableCaching— activates Spring's cache infrastructure and AOP proxies.@Cacheable— stores the method return value; subsequent calls with the same key skip execution.@CacheEvict— removes entries when data changes.@CachePut— always runs the method and updates the cache with the fresh result.RedisCacheManager— maps cache names to Redis keys with optional TTL per cache.
Enable caching with Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring:
data:
redis:
host: 127.0.0.1
port: 6379
cache:
type: redis
redis:
time-to-live: 600000 # 10 minutes default
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> perCache = Map.of(
"books", defaults.entryTtl(Duration.ofHours(1)),
"bestsellers", defaults.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(perCache)
.build();
}
}
Cache reads, evict on writes
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Cacheable(value = "books", key = "#isbn")
public BookDetail getByIsbn(String isbn) {
return bookRepository.findByIsbn(isbn)
.map(BookDetail::from)
.orElseThrow(() -> new BookNotFoundException(isbn));
}
@Cacheable(value = "bestsellers", key = "'top-' + #limit")
public List<BookSummary> getBestsellers(int limit) {
return bookRepository.findTopSellers(limit)
.stream()
.map(BookSummary::from)
.toList();
}
@Caching(evict = {
@CacheEvict(value = "books", key = "#book.isbn"),
@CacheEvict(value = "bestsellers", allEntries = true)
})
public BookDetail updateBook(Book book) {
Book saved = bookRepository.save(book);
return BookDetail.from(saved);
}
}
Programmatic cache access
@Service
public class CatalogWarmupService {
private final CacheManager cacheManager;
private final BookService bookService;
public void preloadFeaturedTitles(List<String> isbns) {
Cache books = cacheManager.getCache("books");
isbns.forEach(isbn -> {
BookDetail detail = bookService.getByIsbn(isbn);
books.put(isbn, detail);
});
}
}
Quick recall
Everything you need if you only revisit this box.
@EnableCaching+spring-boot-starter-data-redis+spring-boot-starter-cacheis the standard stack.@Cacheable("books")keys on method parameters by default; customise withkey = "#isbn".@CacheEvicton update/delete methods prevents stale reads after writes.- Configure per-cache TTL in
RedisCacheManager— bestsellers need shorter TTL than static book metadata. - Redis is shared across instances; pair caching with eviction on every write path that changes cached data.
Test yourself
Answer these before moving on — recall is what makes it stick.