In this article, we’ll see how to use Hazelcast MultiMap to build a scalable caching solution.
What is Hazelcast MultiMap?
MultiMap is a Map-like data structure that stores multiple values for a single key. Hazelcast provides a distributed and thread-safe version of the MultiMap data structure.
Moreover, to store multiple values of a key, Hazelcast MultiMap supports two types of collections – Set and List. A Set does not preserve order and does not allow duplicate or null values. Whereas, a List preserves the order and allows duplicates but does not allow null values.
Why Do We Need Hazelcast MultiMap?
Interestingly, when I came across the MultiMap data structure for the first time, it made me wonder – why do we even need it? Can’t we just use a map that has the values of type Collection to store multiple elements for a given key?
Although MultiMap does the same thing, it is more convenient and intuitive to use. Also, when we look at it in the context of a distributed data structure, it is even more useful. Not only it is easy to use but it also saves us from managing the underlying collection in a distributed setup.
The Example We Are Going To Build
So, let’s dive into the example of the MultiMap now. In this example, we’ll build a solution that is simple and scalable. Definitely, we’ll keep our focus on understanding how the Hazelcast MultiMap works in a distributed environment.
To start with, let’s say, our application needs a service that should return us the meanings of a given word from different dictionaries like Oxford and Cambridge. Since the definition of a word rarely changes, we would also like to cache the data we retrieve from the dictionary sources.
For instance, if we ask for the meaning of the word ‘ubiquitous’, we should get the result like:
ubiquitous -> everywhere, omnipresent
Here, one meaning may come from one dictionary source whereas another may come from the second.
Certainly, to achieve this, we can simply create a Dictionary Service. In this case, the service can call the APIs of different dictionary sources and populate the cache in a MultiMap.
Let’s Make the Example Scalable
Notably, the solution may not be very scalable. This is because we need to change the Dictionary Service in case there is any change in the API specification of any of the dictionary sources. Furthermore, if we need to add or remove dictionary sources, the Dictionary Service needs to change. Unquestionably, this is not adhering to the Open Close Principle.
Therefore, we’ll create separate client services for each of the dictionary sources. These services will form a Hazelcast cluster with our Dictionary Service. So, these client services will call the respective source’s API and also update the cache.
Since each word can be a key and different meanings of the word can be the values, Hazelcast MultiMap is a good option for the cache. Furthermore, we’ll use SET as the collection type to avoid duplicates.
It’s worth noting that, in this case, the Dictionary Service is completely decoupled from the client services. Therefore, it does not need to know how many client services are there. Hence, adding a new service, removing an existing service, or scaling the client services up or down can be done independently.
The follows diagram shows the design we are going to build in this example.
The Code Example
So, without further ado, let’s jump into the coding part now.
Create the Spring Boot Services
Similar to the other Hazelcast examples, let’s first create three Spring boot services using Spring Initializr:
Dictionary Service: This service will expose an API that will take a word as input and return a list of meanings for that word
Cambridge Client Service: This service will simulate enriching the cache with word meanings from Cambridge
Oxford Client Service: This service will simulate enriching the cache with word meanings from Oxford
Hazelcast Configuration
Next, let’s add the Hazelcast dependency to the pom.xml for each service:
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
<version>5.3.6</version>
</dependency>
After that, we’ll create the HazelcastConfig class in the Dictionary Service. Here, we’ll configure the Hazelcast MultiMap to use SET as the collection type, as discussed earlier.
@Configuration
public class HazelcastConfig {
@Value("${dictionary.cache.name}")
private String cacheName;
@Bean
public HazelcastInstance hazelcast() {
Config config = new Config();
MultiMapConfig mmConfig
= new MultiMapConfig()
.setName(cacheName)
.setValueCollectionType( "SET" );
config.addMultiMapConfig(mmConfig);
return Hazelcast.newHazelcastInstance(config);
}
}
After that, let’s copy the same HazelcastConfig class to our other two services as well.
The Dictionary Service API
First, let’s complete the Dictionary Service before moving on to the client services.
As discussed earlier, Dictionary Service will define an API to return the meanings of a given word from different sources.
Endpoint: /dictionary/{word}
Request Type: GET
Let’s create the DictionaryController class:
@RestController
@RequestMapping("/dictionary")
public class DictionaryController {private final DictionaryService service;
@Autowired
public DictionaryController(DictionaryService service) {
this.service = service;
}@GetMapping("/{word}")
public Collection<String> getWordMeanings(@PathVariable String word) {
return service.getWordMeanings(word);
}}
Now, let’s create the DictionaryService class that will return the meanings of the words from the cache:
@Service
public class DictionaryService {
private final HazelcastInstance hzInstance;
private final String dictionaryCacheName;
@Autowired
public DictionaryService(HazelcastInstance hzInstance, @Value("${dictionary.cache.name}") String dictionaryCacheName) {
this.hzInstance = hzInstance;
this.dictionaryCacheName = dictionaryCacheName;
}
public Collection<String> getWordMeanings(String word) {
MultiMap<String, String> dictionaryCache = hzInstance.getMultiMap(dictionaryCacheName);
return dictionaryCache.get(word);
}
}
Notice, how we are getting the instance of the MultiMap and then getting the collection of values for the word.
Testing The Dictionary Service
At this point, we can start the Dictionary Service and test the API. However, we’ll get no results as no data has been loaded in the cache.
The call to http://localhost:8080/dictionary/ubiquitous will return an empty collection, [].
The Cambridge Client Service in Action
Now, it’s time to complete our first client service which is supposed to call the API from Cambridge and enrich the cache with the meanings of the words. However, to keep it simple, we’ll programmatically add a few entries in the MultiMap when this client service starts.
For this, let’s create the DictionaryLoaderService class which will extend the Spring Boot’s ApplicationRunner interface.
@Service
public class DictionaryLoaderService implements ApplicationRunner {
@Autowired
private HazelcastInstance hzInstance;
@Value("${dictionary.cache.name}")
private String dictionaryCacheName;
@Override
public void run(ApplicationArguments args) throws Exception {
MultiMap<String, String> dictionaryCache = hzInstance.getMultiMap(dictionaryCacheName);
dictionaryCache.put("ubiquitous", "everywhere");
dictionaryCache.put("abridge", "shorten");
dictionaryCache.put("abridge", "reduce");
dictionaryCache.put("concede", "admit");
}
}
Also, let’s make sure that we configure the service to run on a different port.
To see the service in action, we can now start both the Dictionary Service and the Cambridge Client Service. When both services are running, we can check the logs to ensure that Hazelcast has created the cluster.
Members {size:2, ver:2} [
Member [172.27.240.1]:5701 - 9ab8114b-3966-4590-8dd2-174a67668ba6
Member [172.27.240.1]:5702 - fd824f4f-c54f-46cf-a2b2-7ec1d215b59f this
]
Now, let’s call our API to see the result. This time we should expect the API to return the data that the Cambridge Client Service has added in the MultiMap.
$ curl -s http://localhost:8080/dictionary/ubiquitous
["everywhere"]
The Oxford Client Service in Action
Similar to the Cambridge Client Service, let’s now create a DictionaryLoaderService class in the Oxford Client Service.
@Service
public class DictionaryLoaderService implements ApplicationRunner {
@Autowired
private HazelcastInstance hzInstance;
@Value("${dictionary.cache.name}")
private String dictionaryCacheName;
@Override
public void run(ApplicationArguments args) throws Exception {
MultiMap<String, String> dictionaryCache = hzInstance.getMultiMap(dictionaryCacheName);
dictionaryCache.put("ubiquitous", "omnipresent");
dictionaryCache.put("abridge", "condense");
dictionaryCache.put("abridge", "shorten");
dictionaryCache.put("concede", "acknowledge");
}
}
Assuming, the other two services are already running, let’s now start the Oxford Client Service. Let’s check the logs to ensure that the service is added to the Hazelcast cluster.
Members {size:3, ver:3} [
Member [172.27.240.1]:5701 - 9ab8114b-3966-4590-8dd2-174a67668ba6
Member [172.27.240.1]:5702 - fd824f4f-c54f-46cf-a2b2-7ec1d215b59f
Member [172.27.240.1]:5703 - 8c52625f-2f94-465e-9d61-a6a1ca63a780 this
]
Finally, let us call our favorite API again to get the meaning of the word ‘ubiquitous’ from both sources this time.
curl -s http://localhost:8080/dictionary/ubiquitous
["everywhere","omnipresent"]
Furthermore, to see how does selecting SET as a collection in the MultiMap work, let’s also call the API for the word ‘abridge’.
$ curl -s http://localhost:8080/dictionary/abridge
["reduce","condense","shorten"]
Note that, both the dictionary client services have added the meaning ‘shorten’ for this word in the MultiMap. However, thanks to the SET, we do not see the value coming twice in the result.
Conclusion
In this article, we’ve looked into the Hazelcast MultiMap. Furthermore, we’ve also discussed why it is useful with the help of a few simple Spring Boot services. We’ve also touched upon the design aspect to understand how we can make a solution more scalable.
You can find the complete code example here.