Spring Boot Caching Made Easy with Hazelcast

Pritha Bag
Globant
Published in
6 min readJul 3, 2023
Photo by Glenn Carstens-Peters on Unsplash

Optimizing application performance is always desired. An effective way to do this is to use caching techniques. Caching stores data that are often used in temporary storage so that applications can fetch it quickly. Caching helps with improving performance and scalability. A popular way of caching for Spring Boot applications is Hazelcast.

What is Hazelcast?

Hazelcast is a powerful open-source Java library that provides distributed and scalable in-memory data storage capabilities. At its core, Hazelcast provides a distributed key-value store, where data is partitioned and distributed across a cluster of nodes. It has a wide range of features such as Time-To-Live (TTL), MaxIdleSeconds, and Write-Through, to help us manage and access data efficiently. It provides distributed data structures such as IMap, Queue, and List to read and write data seamlessly in a distributed manner across clusters.

So let’s get started and harness the power of Spring Boot caching made easy with Hazelcast.

How to implement Hazelcast with SpringBoot?

SpringBoot Application With HazelCast

In this article, we will see how to use the built-in features to easily integrate Hazelcast into your Spring Boot application. We’ll walk through examples that show how to implement Hazelcast in a Spring Boot project. By the end, you will have a clear understanding of how Hazelcast simplifies the caching process for Spring Boot applications .

  1. Setting up Hazelcast with Spring Boot:

To begin, we’ll set up a Spring Boot project and add the necessary dependencies inbuild.gradle file.

dependencies {
implementation group: 'com.hazelcast', name: 'hazelcast-all', version: '4.2.7'
}

2. Add configurations for Hazelcast :

Here I have used the method Hazelcast.newHazelcastInstance(createConfig()); to create a cluster member, and now my application has a distributed cache. I have also passed createConfig() to further customize the configurations.

package com.example.demo.config;

import org.springframework.stereotype.Component;

import com.example.demo.dto.Student;
import com.example.demo.serializer.StudentSerializer;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;

@Component
public class HazelCastConfig {

public static final String STUDENTS = "students";
private final HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(createConfig());

public Student put(String number, Student student) {
IMap<String, Student> map = hazelcastInstance.getMap(STUDENTS);
return map.putIfAbsent(number, student);
}

public Student get(String key) {
IMap<String, Student> map = hazelcastInstance.getMap(STUDENTS);
return map.get(key);
}

public Config createConfig() {
Config config = new Config();
config.addMapConfig(mapConfig());
config.getSerializationConfig().addSerializerConfig(serializerConfig());
return config;
}

private SerializerConfig serializerConfig() {
return new SerializerConfig().setImplementation(new StudentSerializer()).setTypeClass(Student.class);
}

private MapConfig mapConfig() {
MapConfig mapConfig = new MapConfig(STUDENTS);
mapConfig.setTimeToLiveSeconds(360);
mapConfig.setMaxIdleSeconds(400);
return mapConfig;
}

}

In the above code, the method getMap() helps with creating a distributed Map in the cache or returns an existing one, and I have named the cache as STUDENTS.

Time-To-Live (TTL) : We know that cache is just a copy of the actual data and we cannot use it for a long time as the actual data can change. So we need to define the validity time of the data in the cache which is called Time-To-Live. It is always a good practice to set TTL which ensures that the cache gets synced with actual data after an interval of time. With setTimeToLiveSeconds(360) we define how long an entry stays in the cache before getting evicted.

Max Idle: We have to remember that Hazelcast does consume a lot of memory and that it is better to evict data that is not being used frequently. The method setMaxIdleSeconds(400) defines how long the entry stays in the cache without being read.

3. Add Custom Serializer :

Hazelcast serializes data when storing in cache and it is best to use a custom serializer and set it in the config. This helps when I make a change to my Student object which might have been cached before I made that change. I have used StreamSerializer which supports versioning of cache entries (hence avoiding any downtime when I deploy with new changes) and is able to serialize and de-serialize Java objects of different versions at the same time.

package com.example.demo.serializer;

import java.io.IOException;

import com.example.demo.dto.Student;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.StreamSerializer;

public class StudentSerializer implements StreamSerializer<Student> {

@Override
public void write(ObjectDataOutput out, Student object) throws IOException {
out.writeString(object.getName());
out.writeLong(object.getPhoneNumber());
out.writeString(object.getAddress());
out.writeString(object.getSubject());
}

@Override
public Student read(ObjectDataInput in) throws IOException {
return Student.builder().name(in.readString()).phoneNumber(in.readLong()).address(in.readString())
.subject(in.readString()).build();
}

@Override
public int getTypeId() {
return 1;
}

}

We need to remember that the fields in the object must be read in the same order as they are written. The getTypeId() must be unique and greater than or equal to 1 , using which Hazelcast will determine the version of serializer which would be needed to de-serialize an object.

4. Create Student Data :

I have written a simple Controller which interacts with the cache STUDENTS.

package com.example.demo.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.config.HazelCastConfig;
import com.example.demo.dto.Student;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/students")
public class Controller {

private final HazelCastConfig hazelCastConfig;

@PostMapping(path = "/{rollNumber}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(code = HttpStatus.CREATED)
public Student put(@RequestBody Student students, @PathVariable String rollNumber) {
return hazelCastConfig.put(rollNumber, students);
}

@GetMapping(value = "/{rollNumber}", produces = MediaType.APPLICATION_JSON_VALUE)
public Student get(@PathVariable String rollNumber) {
return hazelCastConfig.get(rollNumber);
}
}

In the above code, the POST endpoint inserts records in the cache & the GET endpoint fetches records when provided with an id. Now I am good to run my application. On startup, I am able to see the below line.

Members {size:1, ver:1} [
Member [10.220.92.77]:5701–95e9fc1c-0558–43bc-85b2–247a797f6dcf this
]

This means that my application is able to create a cache member. With Hazelcast, scaling the application becomes seamless. If we run more instances of this application, the member size here will increase, and we will have a cache cluster. As the load increases, the cluster can dynamically scale by adding more nodes.

Application Console

Looking At Another Way Of Implementing Hazelcast :

Now that we have taken a look at creating a SpringBoot application with an embedded Hazelcast member. Let’s go one step further and see how we can implement Hazelcast client and access/update the cache from outside.

For creating this application we will be following the above mentioned points 1 through 3. I will be coming directly to Hazelcast configurations.

Add Configuration for Hazelcast Client:

I have used the method HazelcastClient.newHazelcastClient(creatClientConfig()); for creating a Hazelcast client and it is automatically going to connect with the cache cluster. Now if we put or get data from the Map, the Hazelcast client connects to the cluster to access data.

package com.example.demo.config;

import org.springframework.stereotype.Component;

import com.example.demo.dto.Student;
import com.example.demo.serializer.StudentSerializer;
import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.config.SerializerConfig;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.map.IMap;

@Component
public class CacheConfig {

private static final String STUDENTS = "students";

private HazelcastInstance client = HazelcastClient.newHazelcastClient(creatClientConfig());

public Student put(String key, Student student) {
IMap<String, Student> map = client.getMap(STUDENTS);
return map.putIfAbsent(key, student);
}

public Student get(String key) {
IMap<String, Student> map = client.getMap(STUDENTS);
return map.get(key);
}

private ClientConfig creatClientConfig() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.getSerializationConfig().addSerializerConfig(serializerConfig());
return clientConfig;
}

private SerializerConfig serializerConfig() {
return new SerializerConfig().setImplementation(new StudentSerializer()).setTypeClass(Student.class);
}

}

Now I am good to run my application. On startup, I am able to see the below line, and my application is able to connect with a cache cluster.

2023–05–12 19:32:43.200 INFO 23128 - - [ main] c.h.c.i.c.ClientConnectionManager : hz.client_1 [dev] [4.2.7] Trying to connect to cluster: dev
2023–05–12 19:32:43.203 INFO 23128 - - [ main] c.h.c.i.c.ClientConnectionManager : hz.client_1 [dev] [4.2.7] Trying to connect to [127.0.0.1]:5701
2023–05–12 19:32:44.421 INFO 23128 - - [ main] com.hazelcast.core.LifecycleService : hz.client_1 [dev] [4.2.7] HazelcastClient 4.2.7 (20230207–83309a6) is CLIENT_CONNECTED
Application Console

Conclusion:

As we saw, Hazelcast is a Java library which makes it easy to adopt and set up a cache cluster. It being completely in-memory makes data access very fast. Hazelcast is easily scalable as we only need to add more instances, and the member would automatically join a cluster.

Further Reading:

For more information, head over to the official documentation page of Hazelcast: https://docs.hazelcast.com/home/

--

--