Map and closures : Memoization

Again Maps are a simple and efficient tool.

If we use them in combination with closures we obtain local computation caching.

The most used scenario, for me is caching Database calls only for local computation scope.

For example, I want to compute new customer footprints in a Java stream, but we need some extra information from storage.

Make a closure implementation that it takes a JPA repository, in the constructor and returns a lambda reference.

Function<String, ClientHistory> makeClientHistoryMemoization(ClientHistoryRepo repo) {
Map<String, ClientHistory> cache = new HashedMap();

return (final String clientId) -> {
if (cache.containsKey(clientId)) {
return cache.get(clientId);
}
var clientHistory = repository.findById(clientId);
cache.put(clientId, clientHistory);

return clientHistory;
};
}

Each call using the lambda reference will search in the cache for a response. If is missing the call to the DB is made and the result is cached in the Map.

//JPA repo injected with Spring
final ClientHistoryRepo repository;

public List<ClientFootPrint> generateClientFootprints(List<Client> targetedClients) {
//we get the lambda reference
var queryClientHistory = makeClientHistoryMemoization(repository);

return targetedClients.stream().map(
client -> {
//we use the reference.
//any duplicate client id call will be retrieved from cache
var history = queryClientHistory.apply(client.getId());
return new ClientFootPrint()
.setHisotry(history)
.setInfo(client.getInfo());
}
).collect(Collectors.toUnmodifiableList());
}//Here we release the local cache for GC

The nice part is, as we move out of the method scope the cache is released.

As a note, if we ask for multiple references we will have multiple caches.

var queryClientHistory = makeClientHistoryMemoization(repository);
//some computation
//here we get a new reference, meaning a new cache
queryClientHistory = makeClientHistoryMemoization(repository);

This pattern can be handy when combined with Java 8 Streams.