In this post, we’ll explore how to implement caching in a Spring Boot 3 application using Redis.
· Prerequisites
· Overview
∘ Why Redis for Caching?
∘ Flowchart of the caching mechanism
∘ Setting up a Redis Instance
· Let’s code
∘ Redis Configuration
∘ Caching in the Service Layer
∘ Controller layer
· Test the REST APIs
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites:
- Docker / Docker-compose installed (optional if you’ve already downloaded and installed Redis)
- Spring Boot 3
- Maven 3.6.3 or later
- Java 21
- IntelliJ IDEA, Visual Studio Code, or another IDE
- Postman / insomnia or any other API testing tool.
Overview
Caching is a fundamental performance optimization technique in modern web applications. The Spring Framework provides support for transparently adding caching to an application. At its core, the abstraction applies caching to methods, thus reducing the number of executions based on the information available in the cache.
Why Redis for Caching?
Redis is an open-source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams.
Using Redis as a caching solution can significantly improve application performance, scalability, and responsiveness. Its in-memory architecture, rich data structures, and flexible configuration options make it a popular choice for developers looking to optimize their applications. Whether for caching database queries, session data, or any frequently accessed data, Redis provides a robust and efficient solution.
Flowchart of the caching mechanism

- Client Request:
The application receives an incoming request - Cache Check (Method with Cacheable):
Spring’s cache abstraction checks Redis for cached data using the method signature as the key - Cache Hit (Data is found in the cache, so it has to be fetched from a faster source)
- Data exists in Redis
- Returns cached data immediately
- Bypasses database call completely
4. Cache Miss (Data is not found in the cache, so it has to be fetched from a slower source)
- No valid cached data exists
- Proceeds to execute the actual method
5. Database Operation:
- The main business logic executes
- Typically involves database queries or expensive computations
6. Cache Population:
Successful results are:
- Serialized to JSON
- Stored in Redis with TTL
- Returned to the client
7. Error Handling
Database failures don’t affect cache stability
Setting up a Redis Instance
This story uses the Docker approach to create a Redis instance. Here is the full content of the docker-compose file:
ersion: '3'
services:
redis:
image: redis:latest
container_name: redis_db
ports:
- "6379:6379"
volumes:
- ./data:/data
restart: always
Let’s code
We’ll start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Web, Spring Data Redis, Lombok, H2 Database, Spring Data JPA, and Validation.

Redis Configuration
Spring Data Redis provides an implementation of Spring Framework’s Cache Abstraction in the org.springframework.data.redis.cache package. To use Redis as a backing implementation, add RedisCacheManager to your configuration as follows:
@Configuration
@EnableCaching // Enables Spring's caching capability, allowing annotations like @Cacheable, @CacheEvict, and @CachePut to work.
public class RedisCacheConfig {
@Bean // Defines a Spring bean for RedisCacheManager
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Define cache configuration
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues() // Prevent caching of null values
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(30));
// Create and return a RedisCacheManager with the specified configuration
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
- Serialization:
- Uses
GenericJackson2JsonRedisSerializerto convert Java objects into JSON and vice versa when storing and retrieving from Redis. - Stores type information (
@classfield) for proper deserialization. - This helps avoid issues with default Java serialization and ensures the portability and readability of cached values.
- Handles complex object graphs
- TTL (Time To Live):
- Sets the default expiration time for all cache entries to 30 minutes using
.entryTtl(Duration.ofMinutes(30)). - This means that cached data will automatically expire after 30 minutes.
- After the set, configured TTL expiration timeout, the entry is evicted from the data store regardless. Eviction actions (for example: destroy, invalidate, overflow-to-disk (for persistent stores), etc.) are data store specific.
Next, we need to add the Redis connection in the application.properties or application.yml file:
spring:
cache:
type: REDIS
data:
redis:
host: localhost
port: 6379
Caching in the Service Layer
Now that the cache is configured, we can use the instructions from our services.
@Service
public class AuthorServiceImpl implements AuthorService {
public static final String AUTHOR_CACHE = "authors";
private final AuthorRepository repository;
private final AuthorMapper mapper;
public AuthorServiceImpl(AuthorRepository repo, AuthorMapper mapper) {
this.repository = repo;
this.mapper = mapper;
}
/**
* {@inheritDoc}
*/
@Override
@CachePut(value = AUTHOR_CACHE, key = "#result.id")
public AuthorDTO create(AuthorDTO authorDTO) {
var author = repository.save(mapper.toEntity(authorDTO));
return mapper.toDto(author);
}
/**
* {@inheritDoc}
*/
@Override
@CachePut(value = AUTHOR_CACHE, key = "#result.id")
public AuthorDTO update(AuthorDTO author) {
Author existingAuthor = repository.findById(author.id()).orElseThrow(() -> new DataNotFoundException("Author id not found"));
existingAuthor.setFirstname(author.firstname());
existingAuthor.setLastname(author.lastname());
var author1 = repository.save(existingAuthor);
return mapper.toDto(author1);
}
/**
* {@inheritDoc}
*/
@Override
@Cacheable(value = AUTHOR_CACHE, key = "#id")
public AuthorDTO getOne(Long id) {
var author = repository.findById(id).orElseThrow(() -> new DataNotFoundException("Author id not found"));
return mapper.toDto(author);
}
/**
* {@inheritDoc}
*/
@Override
public List<AuthorDTO> getAll() {
return repository.findAll().stream().map(mapper::toDto).toList();
}
/**
* {@inheritDoc}
*/
@Override
@CacheEvict(value = AUTHOR_CACHE, key = "#id")
public void delete(Long id) {
repository.deleteById(id);
}
}
This service provides typical CRUD operations on Author entities and leverages Spring’s caching annotations to optimize performance and reduce database load.
For caching declaration, Spring’s caching abstraction provides a set of Java annotations:
@Cacheable: Triggers cache population.@CacheEvict: Triggers cache eviction.@CachePut: Updates the cache without interfering with the method execution.@Caching: Regroups multiple cache operations to be applied to a method.@CacheConfig: Shares some common cache-related settings at class level.
For more information, refer to the Spring Declarative Annotation-based Caching documentation.
Controller layer
Create the AuthorController class to expose CRUD endpoints for authors.
@Slf4j
@RestController
@RequestMapping("/api/authors")
public class AuthorController {
private final AuthorService entityService;
public AuthorController(AuthorService entityService) {
this.entityService = entityService;
}
/**
* {@code POST /author} : Create a new author.
*
* @param author the author to create.
* @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new author.
*/
@PostMapping
public ResponseEntity<AuthorDTO> createAuthor(@RequestBody @Valid AuthorDTO author) {
log.debug("REST request to save Author : {}", author);
return new ResponseEntity<>(entityService.create(author), HttpStatus.CREATED);
}
/**
* {@code PUT /author} : Updates an existing author.
*
* @param author the author to update.
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated author,
* or with status {@code 400 (Bad Request)} if the author is not valid,
* or with status {@code 500 (Internal Server Error)} if the author couldn't be updated.
*/
@PutMapping
public ResponseEntity<AuthorDTO> updateAuthor(@Valid @RequestBody AuthorDTO author) {
log.debug("REST request to update Author : {}", author);
AuthorDTO result = entityService.update(author);
return ResponseEntity.ok().body(result);
}
/**
* {@code GET /author} : get all the authors.
*
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of author in body.
*/
@GetMapping
public ResponseEntity<List<AuthorDTO>> getAllAuthor() {
log.debug("REST request to get all authors");
List<AuthorDTO> lst = entityService.getAll();
return new ResponseEntity<>(lst,HttpStatus.OK);
}
/**
* {@code GET /author/:id} : get the "id" author.
*
* @param id the id of the author to retrieve.
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the author, or with status {@code 404 (Not Found)}.
*/
@GetMapping(value = "/{id}")
public ResponseEntity<AuthorDTO> getOneAuthor(@PathVariable("id") Long id) {
log.debug("REST request to get Author : {}", id);
AuthorDTO e = entityService.getOne(id);
return new ResponseEntity<>(e, HttpStatus.OK);
}
/**
* {@code DELETE /author/:id} : delete the "id" author.
*
* @param id the id of the author to delete.
* @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteAuthor(@PathVariable("id") Long id) {
log.debug("REST request to delete Author : {}", id);
entityService.delete(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Test the REST APIs
Now we are all done with our code. We can run our application and test it.
- Create Author
We can see that the result has been saved to the Redis cache. A @class field, which is added by Jackson’s Default Typing automatically, guides Jackson on which class to use in the deserialization process. The TTL duration is automatically refreshed according to the duration defined in the RedisCacheManager bean. Once the time has elapsed, the record is automatically deleted from the Redis cache.

The record is also saved in the local H2 database.
- Get the Author by ID
On the first request, we get an 18 ms response time:
Subsequent responses return in the range of 5 ms to 7 ms consistently:
- Delete the Author
Deleting the record from the database also removes the deleted author from the cache, thus avoiding out-of-date data.
Conclusion
Well done !!.
By leveraging Redis as a distributed cache, you can supercharge your application’s performance, scalability, and user experience.
The complete source code is available on GitHub.
Support me through GitHub Sponsors.
Thank you for reading!! See you in the next post.




