Caching with Micronaut: Superheroes superquick

Caching with Micronaut: Superheroes superquick

Caching with Micronaut

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

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;
    }


}
  1. CacheConfig: It is optional to add if we use @Cacheable but added as just to show
  2. @Cacheable: As the name suggests, tells that this method can be cached cacheName stores names of the caches and parameters 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 on GET. As the method is annotated with Cachaeable, next time whenever the same superhero will be called it will be given from cache rather than DB
  3. CachePut: Will update the cache with the given parameter (key), It will help us when there is a change in superhero properties. It will automatically cache the newer version with the same key.
  4. CacheInvalidate: removes the cache all together with parameter (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

Did you find this article valuable?

Support Kumar Pallav by becoming a sponsor. Any amount is appreciated!