Introduction
Any framework needs to be fast enough to deliver content , Micronaut is no different. In order to deliver content faster, we depend on caching. Micronaut has inbuild support for caching with diffferent cache provider such as Redis, EhCache , Hazelcast etc. Here, we will explore caching implementation with Micronaut. If you wan't the full source code , you can find it below.
Source code : https://github.com/CODINGSAINT/super-heroes/tree/05CacheWithMicronaut
Let's explore micronaut caching.
Table of Content
- Goal
- Prerequisite
Goal
The goal of this article is simple. We need a service that demonstrates caching capabilities.
- System must cache the superheroes whenever they are called.
- Every next call to the service should not be hitting the database but our caches.
- Every update on superhero API should also update the cache object.
- Once any superhero is deleted cache should be invalidated and next time any call to superhero GET API should fetch from the database again.
Prerequisite
We have created the application with a reactive database (postgres) . We will use same for adding caching capabilities to our projects. You can check the previous where we added testcontains at https://blog.pallav.dev/realtime-testing-micronaut-postgres-and-testcontainer You can also check the entire series at https://blog.pallav.dev/series/micronaut
Approach
Adding the cache dependencies of micronaut
Add below dependencies to pom.xml
<!-- 05 Cache Configs -->
<dependency>
<groupId>io.micronaut.cache</groupId>
<artifactId>micronaut-cache-caffeine</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.cache</groupId>
<artifactId>micronaut-cache-management</artifactId>
</dependency>
<!-- 05 cache Configs ends-->
Adding the cache properties to yml
Let us add the properties to application yml ,which will tell the caches the system will configure
#cache related
micronaut:
caches:
superheroes:
charset: 'UTF-8'
superhero:
charset: 'UTF-8'
endpoints:
caches:
enabled: true
sensitive: false
Creating Service layer for cache Database (postgres) and Cache
Till now we have been injecting DAO to controller , now it's a good use case to create a service layer which will do the talking to cache & db . It act as bridge between controllers and others. Let us add the service layer . Create a Service class as following with Repository and Cache Injected to it
@Singleton
@CacheConfig("superheroes")//1
public class SuperheroService {
private static final Logger logger = LoggerFactory.getLogger(SuperheroService.class);
protected ReactiveSuperheroRepository reactiveSuperheroRepository;
@Inject
private CacheManager cacheManager;
SuperheroService(ReactiveSuperheroRepository superheroRepository) {
this.reactiveSuperheroRepository = superheroRepository;
}
public Flux<Superhero> superheroes() {
logger.info("Fetching all of the superheroes in universe ");
return reactiveSuperheroRepository.findAll();
}
@Cacheable( cacheNames = {"superheroes"}, parameters ={"id"} )//1
public Mono<Superhero> superheroesById(Long id) {
logger.info("Finding the saviour id {} ", id);
return reactiveSuperheroRepository.findById(id);
}
public Publisher<Superhero> create(Superhero superhero) {
logger.info("Adding a new saviour {} ", superhero);
var created = reactiveSuperheroRepository.save(superhero);
return created;
}
@CachePut( cacheNames = "superheroes", parameters = "id")//2
public Publisher<Superhero> update(Superhero superhero, Long id) {
logger.info("Updating the old saviour {} ", superhero);
var updated=reactiveSuperheroRepository.update(superhero);
return updated;
}
@CacheInvalidate(cacheNames = {"superheroes"},parameters = "id" ,async = true)//4
public Publisher<Long> delete(Long id) {
var deleted= reactiveSuperheroRepository.deleteById(id);
return deleted;
}
}
CacheConfig
: It is optional to add if we use@Cacheable
but added as just to show@Cacheable
: As the name suggests, tells that this method can be cachedcacheName
stores names of the caches andparameters
tells the id with which cache will be created. If the method has only one parameter, it can be avoided. POST method will add the superhero and once it will be retrieved from the database onGET
. As the method is annotated withCachaeable
, next time whenever the same superhero will be called it will be given from cache rather than DBCachePut
: Will update the cache with the givenparameter
(key), It will help us when there is a change in superhero properties. It will automatically cache the newer version with the same key.CacheInvalidate
: removes the cache all together withparameter
(key). This will invalidate the superhero so when it will be asked to give the same superhero again, we will not be getting the value from cache and a DB hit will happen.
Injecting Service layer to controller
Till now we have been calling the database from Controller but now we will call the Service layer, so we will inject the same and remove the repository (which is moved to service)
@Controller("rx/")
public class ReactiveSuperHeroController {
private static final Logger logger = LoggerFactory.getLogger(SuperHeroController.class);
private SuperheroService service;
public ReactiveSuperHeroController(SuperheroService service) {
this.service = service;
}
@Get("superheroes")
public Flux<Superhero> superheroes() {
return service.superheroes();
}
@Get("superhero/{id}")
public Mono<Superhero> superheroesById(Long id) {
return service.superheroesById(id);
}
@Post("/superhero")
Single<Superhero> create(@Valid Superhero superhero) {
logger.info("Call to add a new saviour {} ", superhero);
return Single.fromPublisher(service.create(superhero));
}
@Put("superhero")
Single<Superhero> update( @Valid Superhero superhero) {
return Single.fromPublisher(service.update(superhero, superhero.id()));
}
@Delete("superhero/{id}")
Single<HttpResponse<?>> delete(@NotNull Long id) {
return Single
.fromPublisher(service.delete(id))
.map(deleted->deleted>0?HttpResponse.noContent():HttpResponse.notFound());
}
}
Running the project
Now we can run the project and Create few Superheroes and do CRUD operations. We will keep an eye on logs to see if the method (GET) is being called multiple times. It is expected that the Cachable method will be called only when the call for the superhero is for the first time.
Below is the CURL request to create (POST)
curl --location --request POST 'http://localhost:8080/rx/superhero' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Justice Hammer",
"prefix": "Captain",
"suffix": "Walker",
"power": "Lightening Thunder"
}'
Now we will hit GET multiple times but logs will print only once from the service layer
curl --location --request GET 'http://localhost:8080/rx/superhero/1'
curl --location --request GET 'http://localhost:8080/rx/superhero/1'
curl --location --request GET 'http://localhost:8080/rx/superhero/1'
We can see in the logs below that the service layer is called once, post that it is not called as the values have been returned from the cache.
We can also update and check still, the GET log will not print as an update on the same hero will be also updated in the cache. Once deleted it will try again to check from DB as the cache is invalidated.
The logs of the above operation will be
our Superhero[id=null, name=Justice Hammer, prefix=Captain, suffix=Walker, power=Lightening Thunder]
00:42:06.300 [default-nioEventLoopGroup-1-3] INFO c.c.service.SuperheroService - Adding a new saviour Superhero[id=null, name=Justice Hammer, prefix=Captain, suffix=Walker, power=Lightening Thunder]
00:44:44.132 [default-nioEventLoopGroup-1-3] INFO c.c.service.SuperheroService - Finding the saviour id 1
Conclusion
We saw that caching has reduced the load on DB and significantly improved performance. If you have not noticed performance benefits, check time taken with the first call and the subsequent (Cacheable
) method calls