DEV Community

Cover image for Integrate Spring Boot Cache with a database that Spring does not support
Zakaria Shahen
Zakaria Shahen

Posted on • Edited on

Integrate Spring Boot Cache with a database that Spring does not support

Sometimes you need to use Spring Cache with your own data source, but Spring Boot Cache does not support it fortunately, Spring embraces the Open-Closed Principle, so it's simple to expand it to fit your unique use cases.

Let's look at how to expand the Spring Boot cache to support a new data source.

How to expand the Spring Boot Cache capability by supporting MongoDB or DynamoDB

You must register your custom CacheManger bean and implement the Cache interface in order to accomplish that. Let's see how it looks.

Implement Cache interface

A Cache interface is responsible for defining common cache actions such as saving and retrieving cached data from a database.

It is necessary to implement the TTL mechanism, perhaps at the database level. For instance, TTL index in MonogoDB or TTL in DynamoDB

package dev.zakaria.springcache.config;

import dev.zakaria.springcache.repository.SpringCacheRepository;
import lombok.AllArgsConstructor;
import org.springframework.cache.Cache;

import java.util.concurrent.Callable;

@AllArgsConstructor
public class MongoCache implements Cache {

    private final String cacheName;
    private final SpringCacheRepository springCacheRepository;

    @Override
    public String getName() {
        return cacheName;
    }

    @Override
    public Object getNativeCache() {
        throw new UnsupportedOperationException();
    }

    @Override
    public ValueWrapper get(Object key) {
        // TODO: implement your data access logic here 
        return null;
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        // TODO: implement your data access logic here 
        return null;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        // TODO: implement your data access logic here 
        return null;
    }

    @Override
    public void put(Object key, Object value) {
        // TODO: implement your data access logic here 
    }

    @Override
    public void evict(Object key) {
        // TODO: implement your data access logic here 
    }

    @Override
    public void clear() {
        // TODO: implement your data access logic here 
    }
}

Enter fullscreen mode Exit fullscreen mode

Register your customized CacheManager bean

The AbstractCacheManager class is responsible for instantiating cache objects and registering them in Spring IoC.
By implementing AbstractCacheManager and annotating it with @Component, you notify Spring which class to look at when doing caching operations.

package dev.zakaria.springcache.config;

import dev.zakaria.springcache.repository.SpringCacheRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.support.AbstractCacheManager;
import org.springframework.stereotype.Component;

import java.util.Collection;


@Component
public class MongoCacheManager extends AbstractCacheManager {
    private final Collection<String> cacheNames;
    private final SpringCacheRepository springCacheRepository;

    public MongoCacheManager(@Value("${spring.cache.cache-names}") Collection<String> cacheNames, SpringCacheRepository springCacheRepository) {
        this.cacheNames = cacheNames;
        this.springCacheRepository = springCacheRepository;
    }

    @Override
    protected Collection<? extends Cache> loadCaches() {
        return cacheNames.stream()
                .map(it -> new MongoCache(it, springCacheRepository))
                .toList();
    }
}
Enter fullscreen mode Exit fullscreen mode

Use @Cacheable

You can now utilize usual spring cache annotations such as @Cacheable. But first, we need to put the cache name in application.properties

spring.cache.cache-names=demo,demo2
Enter fullscreen mode Exit fullscreen mode

Ensure that Spring Boot automatically configures for caching by adding the @EnableCaching annotation to any of the configuration classes or Application main class.

package dev.zakaria.springcache.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Log4j2
public class DemoController {

    @GetMapping("/demo/{userId}")
    @Cacheable("demo")
    public PersonalizeResponse demo(@PathVariable String userId) {
        return fetchPersonalizeData(userId);
    }

    private PersonalizeResponse fetchPersonalizeData(String userId) {
        log.info("calling third-party API for user id {}", userId);
        return new PersonalizeResponse("Obtain personalized information from a third-party API. Since each API call we make costs money, what if we saved this response for later requests?", userId);
    }

    public record PersonalizeResponse(String result, String userId) {}
}
Enter fullscreen mode Exit fullscreen mode

While use Webflux Adding .cache() to your Flux or Mono is necessary if you were using the Spring Framework prior version 6.1.

Modify your Cache implementation to use Spring Framework 6.1 and Webflux

Prior to Spring Framework 6.1, fetch logic had to be implemented in a blocked approach.
However, you can use reactive or non-block style when you modify your cache implementation to fit Spring Framework 6.1.

All you have to do is override the next retrieve() methods.

package dev.zakaria.springcache.config;

import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;

import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;

@AllArgsConstructor
public class MongoCache implements Cache {

    // ... methods 

    @Override
    public CompletableFuture<?> retrieve(Object key) {
        // TODO: Implement reactive data access logic here
        return Cache.super.retrieve(key);
    }

    @Override
    public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
        // TODO: Implement reactive data access logic here
        return Cache.super.retrieve(key, valueLoader);
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it 🎉

Top comments (0)