Multi-Tenant Cache Store using Custom RedisCacheManager
Tags: redis, springboot, multitenant, tutorial
Published: 2020-09-25 03:25:04

Idea behind this post is to demonstrate how easily tenant specific cache-store in a *multi-tenant* application can be built using Spring annotations,Custom **RedisCacheManager**.

##### Why Caching ?
There are multiple benefits of caching.One key benefit is to reduce database access thus making access to data much faster and less expensive.
For more details on _Redis cache use case_ refer [this](https://redislabs.com/solutions/use-cases/caching/).

##### Multi-Tenant Application Design
As per Wikipedia,[Multitenancy](https://en.wikipedia.org/wiki/Multitenancy) refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants.
Here we will follow shared database(all tenants share same `database->schema->table`) approach. We will use just one entity as follow.
```java
@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
@DynamicInsert
@DynamicUpdate
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantKey", type = "string")})
@Filter(name = "tenantFilter", condition = "tenant = :tenantKey")
public class Customer implements Serializable {
    @Id
    String id;
    String name;
    //Identifier to distinguish each tenant
    String tenant;
}
```
While invoking any APIs mandatory http header parameter 
**x-tenant-id** will be supplied.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/t82pemzfs0oecs4exfwq.png)
> * For demo purpose we are using simple tenantKey.
> * Recommended to use an alphanumeric id which is unique and difficult to guess(_e.g. 64b2a7b330614cd8804bcd93b72a069e_).

##### Enable Caching
spring boot provides simple annotation based configuration to enable caching([more details](https://www.baeldung.com/spring-cache-tutorial)).For Starting Redis in Local System
> docker run --name local-redis -p 6379:6379 -d redis

Now cache the result of below repository method.Just by adding `@Cacheable`
```java
import java.util.List;

@Repository
public interface CustomerRepository extends JpaRepository<Customer, String> {
    @Override
    @Cacheable(value = "customers")
    List<Customer> findAll();
}
```
***

##### Implementation(No custom manager)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7ay9dgm0tk4rjd496uaz.png)
* Loading data from database into cache store.
```json
 [
   "customers"
 ]
```
 
* 1-1 Mapping between database table to cache store.
 * Fetch data from cache, then filter it based on the supplied 
 tenantId and return the response.
 * Additional custom processing logic.
* Assume a scenario where few tenants didn't access the application for some hours, but due to above implementation data will be available in cache store.

***

##### Now Lets See The Implementation(With custom manager)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ymk7e00pqsgvq34ii26j.png)

* Idea is to prefix all the cache stores with the tenantId.
```json
      [
         "tenant1_customers",
         "tenant2_customers",
         "tenant3_customers",
         "tenant4_customers",
         "tenant5_customers"
       ]

```

**How Can we Implement This ???**
> CustomCacheManger Extending RedisCacheManager
```java
@Slf4j
public class CustomCacheManager extends RedisCacheManager {
    public CustomCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
        RedisCacheManager.builder()
                .cacheWriter(cacheWriter)
                .cacheDefaults(defaultCacheConfiguration)
                .build();
    }

    /**
     * @param name
     * @return Prefix the cache store name with the TENANT KEY
     * For SUPER ADMIN no prefix applied
     */
    @Override
    public Cache getCache(String name) {
        log.info("Inside getCache:" + name);
        String tenantId = TenantContext.getTenant().get();
        if (tenantId.equals(Constants.SUPER_ADMIN_TENANT)) {
            return super.getCache(name);
        } else if (name.startsWith(tenantId)) {
            return super.getCache(name);
        }
        return super.getCache(tenantId + "_" + name);
    }

}
```
> Configuration class to create CacheManger Bean
```java
@Configuration
@EnableCaching
public class RedisConfigurationCustom {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()))
                //Configure in Property file as per use case, hardcoded just for demo
                .entryTtl(Duration.ofSeconds(600));
    }

    @Bean
    public RedisCacheWriter redisCacheWriter() {

        return RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory());

    }

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {

        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisCacheManager redisCacheManager() {
        return new CustomCacheManager(redisCacheWriter(), redisCacheConfiguration());
    }
```

> Congratulations now the basic implementation is complete

---

> **_Let's see what is going on_**
* Using `HandlerInterceptor` , tenant value is fetched from Request Headers and set in ThreadLocal.
* We have created a `CustomCacheManager` by extending `RedisCacheManager`,then override `getCache(String name)` implementation to manipulate the name.
* As you can see we are storing the supplied TenantId in [ThreadLocal](https://www.baeldung.com/java-threadlocal) ,hence we can use it inside *getCache()*,the one we are overriding to manipulate the cache name.
* Anyplace where *cache name(e.g. customers)* is specified within `@cachable` *getCache()* method will be invoked and return name with tenantId prefixed to it for further use within the application.
* For some use cases we might need to call the getCache method explicitly using the `CustomCacheManager`, hence to avoid multiple prefixing additional checks are used.
 * One such use case is having a scheduled process as super admin to clean cache stores for all the tenant.


This doesn't have all the answers you might want, but I **hope this is a helpful starting point**.



{% github Sidhanta-Samantaray/multitenant-redis-caching  %}


### Reference Documents
* [Database Caching Strategies Using Redis](https://d0.awsstatic.com/whitepapers/Database/database-caching-strategies-using-redis.pdf)
* [Introduction to Lettuce – the Java Redis Client](https://www.baeldung.com/java-redis-lettuce)
* [Spring Data Redis’s Property-Based Configuration](https://www.baeldung.com/spring-data-redis-properties)
* [Redis using Docker](https://hub.docker.com/_/redis)