Caching in Spring Boot RESTful Service: Part 1
0 CommentsLast Updated on January 14, 2021 by jt
Why Use Caching?
When data stored in some database is requested simultaneously from a large user base, the system can get overwhelmed. This happens because for each request the application has to retrieve the data from the database. As the number of simultaneous requests keeps on increasing, the performance of the system degrades increasing latency. You can address this problem using caching.
In this first part of the series on caching, I will explain how to cache frequently retrieved data in a Spring Boot RESTful API.
The Sample Application
I have a bare minimum Spring Boot REST API that enables users to add products and retrieve all products at one go. As it is apparent, I will set up caching for the operation of retrieving all products. I want the application to return all products from the cache instead of querying the database for each request. To enable caching add the following dependency to your pom.xml file.
Here is the caching dependency in the pom.xml file.
<dependency>; <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
The next step is to enable caching in the application by adding the @EnableCaching
class-level annotation.
@EnableCaching @SpringBootApplication public class RestServiceForProductApplication { public static void main(String[] args) { SpringApplication.run(RestServiceForProductApplication.class, args); } }
Implement Caching for Product Retrieval
The application has a ProductServiceImpl
class where we will enable caching. The code that implements caching is this:
ProductServiceImpl.java
@CacheConfig(cacheNames = "product") @Service public class ProductServiceImpl implements ProductService { private ProductRepository productRepository; public ProductServiceImpl() { } @Autowired public void setProductRepository(ProductRepository productRepository) { this.productRepository = productRepository; } @Autowired public ProductServiceImpl(ProductRepository productRepository) { this.productRepository = productRepository; } }
In the preceding code, the class is marked with the @CacheConfig(cacheNames = "product")
annotation.
It is a class-level annotation that provides common cache-related settings. It tells the string where to store the cache for the class. In the example provided above, “product” is the name of the cache.
Now, let’s add a service method addProduct()
for adding products to the database.
@Caching(evict = {@CacheEvict(value = "allproductcache", allEntries = true), @CacheEvict(value = "productcache", key = "#product.id") }) @Override public Product addProduct(Product product) { return productRepository.save(product); }
In the preceding code, @Caching
annotation is required when we need both @CachePut
and @CacheEvict
at the same time. In other words, when we want to use multiple annotations of the same type we use this annotation. When you want to remove or evict cache of previously loaded master data you will have to use @CacheEvict
. If you want to remove all entries of the cache then you need to use allEntries = true
.
Finally, let’s implement the service method to retrieve all products.
@Cacheable(value = "allproductcache") @Override public List<Product> getAllProducts() { System.out.println("Data is retrieved from database "); return (List<Product>) productRepository.findAll(); }
@Cacheable
is a method level annotation. It defines a cache for a method’s return value. You can also add a cache name by using the value attribute. You can also specify a unique key to identify values in the cache.
Now that our implementation is ready let’s test the caching functionality.
Test for Caching
To test the application, I am using Spring Test with JUnit 5 and Mockito.
The test code is this:
@ExtendWith(MockitoExtension.class) @SpringBootTest public class ProductServiceTest { @Mock private ProductRepository productRepository; @Autowired @InjectMocks private ProductServiceImpl productService; private Product product1; private Product product2; @BeforeEach public void setUp() { product1 = new Product(1, "Bread", 20); product2 = new Product(2, "jam", 140); } @AfterEach public void tearDown() { product1 = product2 = null; } @Test void givenCallToGetAllUsersThenShouldReturnListOfAllProduct() { productService.addProduct(product1); productService.addProduct(product2); productService.getAllProducts(); productService.getAllProducts(); productService.getAllProducts(); productService.getAllProducts(); verify(productRepository, times(1)).findAll(); } }
In the test code, we are mocking the product repository. The test case adds to products and makes four calls to retrieve all the products. Without caching this would have involved four calls to the database. But let’s verify that only a single call happens instead of four calls.
This is done in the verify method on Line 38.
Now, let’s run the test.
The output of the test is this.
As you can see the test passes. Because of caching only a single call got made to the repository.
Summary
In this post, you can see that whenever products are added to the database instead of querying the database for retrieving products for each incoming request, cache comes into play.
Now, consider a scenario where you need to delete a product. The cache has to reflect the change as well. Else, the deleted product will still be present in the cache and returned to users. The same thing will happen when a product gets updated. I will be discussing how to manage such scenarios in Part 2 of this series on caching.
You can find the source code of this post on Github.
For in-depth knowledge of the Spring Framework check my Udemy Best Seller Course Spring Framework 5: Beginner to Guru