Multi-Tenancy Implementation Using Spring Boot, MongoDB, and Redis

In this tutorial, we will learn how to implement multi-tenancy in a Spring Boot application with MongoDB and Redis.

Prerequisites

What Is Multi-Tenancy?

Multi-tenancy is a software architecture in which a single instance of a software application serves multiple customers. Everything should be shared, except for the different customers’ data, which should be properly separated. Despite the fact that they share resources, tenants aren’t aware of each other, and their data is kept totally separate. Each customer is called a tenant.

Software-as-a-service (SaaS) offerings are an example of multitenant architecture. More explanations.

Multi-Tenancy Models

Three principal architectural patterns for Multi Tenancy can be identified, which differs in the degree of (physical) separation of the tenant’s data.

  1. Database per Tenant: Each Tenant has its own database and is isolated from other tenants.
  2. Shared Database, Shared Schema: All Tenants share a database and tables. Every table has a Column with the Tenant Identifier, that shows the owner of the row.
  3. Shared Database, Separate Schema: All Tenants share a database, but have their own database schemas and tables.

Get Started

In this tutorial, we'll implement multi-tenancy based on a database per tenant.

We will start by creating a simple Spring Boot project from start.spring.io, with following dependencies:

XML
 




x
23


 
1
<dependencies>
2
    <dependency>
3
        <groupId>org.springframework.boot</groupId>
4
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
5
    </dependency>
6
    <dependency>
7
        <groupId>org.springframework.boot</groupId>
8
        <artifactId>spring-boot-starter-web</artifactId>
9
    </dependency>
10
    <dependency>
11
        <groupId>org.springframework.boot</groupId>
12
        <artifactId>spring-boot-starter-data-redis</artifactId>
13
    </dependency>
14
    <dependency>
15
        <groupId>redis.clients</groupId>
16
        <artifactId>jedis</artifactId>
17
    </dependency>
18
    <dependency>
19
        <groupId>org.projectlombok</groupId>
20
        <artifactId>lombok</artifactId>
21
        <optional>true</optional>
22
    </dependency>
23
</dependencies>



Resolving the Current Tenant ID

The tenant id needs to be captured for each client request. To do so, We’ll include a tenant Id field in the header of the HTTP request. 

Let's add an interceptor that capture the Tenant Id from an http header X-Tenant.

Java
 




xxxxxxxxxx
1
29


 
1
@Slf4j
2
@Component
3
public class TenantInterceptor implements WebRequestInterceptor {
4
 
          
5
    private static final String TENANT_HEADER = "X-Tenant";
6
 
          
7
    @Override
8
    public void preHandle(WebRequest request) {
9
        String tenantId = request.getHeader(TENANT_HEADER);
10
 
          
11
        if (tenantId != null && !tenantId.isEmpty()) {
12
            TenantContext.setTenantId(tenantId);
13
            log.info("Tenant header get: {}", tenantId);
14
        } else {
15
            log.error("Tenant header not found.");
16
            throw new TenantAliasNotFoundException("Tenant header not found.");
17
        }
18
    }
19
 
          
20
    @Override
21
    public void postHandle(WebRequest webRequest, ModelMap modelMap) {
22
        TenantContext.clear();
23
    }
24
 
          
25
    @Override
26
    public void afterCompletion(WebRequest webRequest, Exception e) {
27
 
          
28
    }
29
}



TenantContext is a storage that contains a ThreadLocal variable. The ThreadLocal can be considered as a scope of access, like a request scope or session scope.

By storing the tenantId in a ThreadLocal we can be sure that every thread has its own copy of this variable and that the current thread has no access to another tenantId:

Java
 




xxxxxxxxxx
1
18


 
1
@Slf4j
2
public class TenantContext {
3
 
          
4
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
5
 
          
6
    public static void setTenantId(String tenantId) {
7
        log.debug("Setting tenantId to " + tenantId);
8
        CONTEXT.set(tenantId);
9
    }
10
 
          
11
    public static String getTenantId() {
12
        return CONTEXT.get();
13
    }
14
 
          
15
    public static void clear() {
16
        CONTEXT.remove();
17
    }
18
}



Setup Tenant Datasources

In our architecture, we have a Redis instance that represents the master database where all the tenant database information is centralized. So, from each tenant id provided, the database connection information is retrieved in master database.

RedisDatasourceService.java is the class responsible for managing all interactions with the master database.

Java
 




xxxxxxxxxx
1
101


 
1
@Service
2
public class RedisDatasourceService {
3
 
          
4
 
          
5
    private final RedisTemplate redisTemplate;
6
 
          
7
    private final ApplicationProperties applicationProperties;
8
    private final DataSourceProperties dataSourceProperties;
9
 
          
10
    public RedisDatasourceService(RedisTemplate redisTemplate, ApplicationProperties applicationProperties, DataSourceProperties dataSourceProperties) {
11
        this.redisTemplate = redisTemplate;
12
        this.applicationProperties = applicationProperties;
13
        this.dataSourceProperties = dataSourceProperties;
14
    }
15
 
          
16
    /**
17
     * Save tenant datasource infos
18
     *
19
     * @param tenantDatasource data of datasource
20
     * @return status if true save successfully , false error
21
     */
22
    public boolean save(TenantDatasource tenantDatasource) {
23
        try {
24
            Map ruleHash = new ObjectMapper().convertValue(tenantDatasource, Map.class);
25
            redisTemplate.opsForHash().put(applicationProperties.getServiceKey(), String.format("%s_%s", applicationProperties.getTenantKey(), tenantDatasource.getAlias()), ruleHash);
26
            return true;
27
        } catch (Exception e) {
28
            return false;
29
        }
30
    }
31
 
          
32
    /**
33
     * Get all of keys
34
     *
35
     * @return list of datasource
36
     */
37
    public List findAll() {
38
        return redisTemplate.opsForHash().values(applicationProperties.getServiceKey());
39
    }
40
 
          
41
    /**
42
     * Get datasource
43
     *
44
     * @return map key and datasource infos
45
     */
46
    public Map<String, TenantDatasource> loadServiceDatasources() {
47
 
          
48
        List<Map<String, Object>> datasourceConfigList = findAll();
49
 
          
50
        // Save datasource credentials first time
51
        // In production mode, this part can be skip
52
        if (datasourceConfigList.isEmpty()) {
53
 
          
54
            List<DataSourceProperties.Tenant> tenants = dataSourceProperties.getDatasources();
55
            tenants.forEach(d -> {
56
                TenantDatasource tenant = TenantDatasource.builder()
57
                        .alias(d.getAlias())
58
                        .database(d.getDatabase())
59
                        .host(d.getHost())
60
                        .port(d.getPort())
61
                        .username(d.getUsername())
62
                        .password(d.getPassword())
63
                        .build();
64
 
          
65
                save(tenant);
66
            });
67
 
          
68
        }
69
 
          
70
        return getDataSourceHashMap();
71
    }
72
 
          
73
    /**
74
     * Get all tenant alias
75
     *
76
     * @return list of alias
77
     */
78
    public List<String> getTenantsAlias() {
79
        // get list all datasource for this microservice
80
        List<Map<String, Object>> datasourceConfigList = findAll();
81
 
          
82
        return datasourceConfigList.stream().map(data -> (String) data.get("alias")).collect(Collectors.toList());
83
    }
84
 
          
85
    /**
86
     * Fill the data sources list.
87
     *
88
     * @return Map<String, TenantDatasource>
89
     */
90
    private Map<String, TenantDatasource> getDataSourceHashMap() {
91
 
          
92
        Map<String, TenantDatasource> datasourceMap = new HashMap<>();
93
 
          
94
        // get list all datasource for this microservice
95
        List<Map<String, Object>> datasourceConfigList = findAll();
96
 
          
97
        datasourceConfigList.forEach(data -> datasourceMap.put(String.format("%s_%s", applicationProperties.getTenantKey(), (String) data.get("alias")), new TenantDatasource((String) data.get("alias"), (String) data.get("host"), (int) data.get("port"), (String) data.get("database"), (String) data.get("username"), (String) data.get("password"))));
98
 
          
99
        return datasourceMap;
100
    }
101
}



For this tutorial, we have populated in the tenant information from a yml file(tenants.yml).In production mode, it is possible to create endpoints to save tenant information in the master database.

In order to be able to dynamically switch to the connection to a mongo database, we create a MultiTenantMongoDBFactory class which extends the SimpleMongoClientDatabaseFactory class of org.springframework.data.mongodb.core. It will return a MongoDatabase instance associated with the currently Tenant.

Java
 




xxxxxxxxxx
1
15


 
1
@Configuration
2
public class MultiTenantMongoDBFactory extends SimpleMongoClientDatabaseFactory {
3
 
          
4
    @Autowired
5
    MongoDataSources mongoDataSources;
6
 
          
7
    public MultiTenantMongoDBFactory(@Qualifier("getMongoClient") MongoClient mongoClient, String databaseName) {
8
        super(mongoClient, databaseName);
9
    }
10
 
          
11
    @Override
12
    protected MongoDatabase doGetMongoDatabase(String dbName) {
13
        return mongoDataSources.mongoDatabaseCurrentTenantResolver();
14
    }
15
}



We need to initialize the MongoDBFactoryMultiTenantconstructor with default parameters (MongoClient and databaseName).

This is a transparent mechanism for retrieving the Current Tenant. 

Java
 




xxxxxxxxxx
1
73


 
1
@Component
2
@Slf4j
3
public class MongoDataSources {
4
 
          
5
 
          
6
    /**
7
     * Key: String tenant alias
8
     * Value: TenantDatasource
9
     */
10
    private Map<String, TenantDatasource> tenantClients;
11
 
          
12
    private final ApplicationProperties applicationProperties;
13
    private final RedisDatasourceService redisDatasourceService;
14
 
          
15
    public MongoDataSources(ApplicationProperties applicationProperties, RedisDatasourceService redisDatasourceService) {
16
        this.applicationProperties = applicationProperties;
17
        this.redisDatasourceService = redisDatasourceService;
18
    }
19
 
          
20
 
          
21
    /**
22
     * Initialize all mongo datasource
23
     */
24
    @PostConstruct
25
    @Lazy
26
    public void initTenant() {
27
        tenantClients = new HashMap<>();
28
        tenantClients = redisDatasourceService.loadServiceDatasources();
29
    }
30
 
          
31
    /**
32
     * Default Database name for spring initialization. It is used to be injected into the constructor of MultiTenantMongoDBFactory.
33
     *
34
     * @return String of default database.
35
     */
36
    @Bean
37
    public String databaseName() {
38
        return applicationProperties.getDatasourceDefault().getDatabase();
39
    }
40
 
          
41
    /**
42
     * Default Mongo Connection for spring initialization.
43
     * It is used to be injected into the constructor of MultiTenantMongoDBFactory.
44
     */
45
    @Bean
46
    public MongoClient getMongoClient() {
47
        MongoCredential credential = MongoCredential.createCredential(applicationProperties.getDatasourceDefault().getUsername(), applicationProperties.getDatasourceDefault().getDatabase(), applicationProperties.getDatasourceDefault().getPassword().toCharArray());
48
        return MongoClients.create(MongoClientSettings.builder()
49
                .applyToClusterSettings(builder ->
50
                        builder.hosts(Collections.singletonList(new ServerAddress(applicationProperties.getDatasourceDefault().getHost(), Integer.parseInt(applicationProperties.getDatasourceDefault().getPort())))))
51
                .credential(credential)
52
                .build());
53
    }
54
 
          
55
    /**
56
     * This will get called for each DB operations
57
     *
58
     * @return MongoDatabase
59
     */
60
    public MongoDatabase mongoDatabaseCurrentTenantResolver() {
61
        try {
62
            final String tenantId = TenantContext.getTenantId();
63
 
          
64
            // Compose tenant alias. (tenantAlias = key + tenantId)
65
            String tenantAlias = String.format("%s_%s", applicationProperties.getTenantKey(), tenantId);
66
 
          
67
            return tenantClients.get(tenantAlias).getClient().
68
                    getDatabase(tenantClients.get(tenantAlias).getDatabase());
69
        } catch (NullPointerException exception) {
70
            throw new TenantAliasNotFoundException("Tenant Datasource alias not found.");
71
        }
72
    }
73
}



Test

Let's create an example of CRUD with an Employee document.

Java
 




xxxxxxxxxx
1
17


 
1
@Builder
2
@Data
3
@AllArgsConstructor
4
@NoArgsConstructor
5
@Accessors(chain = true)
6
@Document(collection = "employee")
7
public class Employee  {
8
 
          
9
    @Id
10
    private String id;
11
 
          
12
    private String firstName;
13
 
          
14
    private String lastName;
15
 
          
16
    private String email;
17
}



Also we need to create EmployeeRepository, EmployeeService and EmployeeController. For testing, we load dummy data into each tenant database when the app starts.

Java
 




xxxxxxxxxx
1
21


 
1
@Override
2
public void run(String... args) throws Exception {
3
    List<String> aliasList = redisDatasourceService.getTenantsAlias();
4
    if (!aliasList.isEmpty()) {
5
        //perform actions for each tenant
6
        aliasList.forEach(alias -> {
7
            TenantContext.setTenantId(alias);
8
            employeeRepository.deleteAll();
9
 
          
10
            Employee employee = Employee.builder()
11
                    .firstName(alias)
12
                    .lastName(alias)
13
                    .email(String.format("%s%s", alias, "@localhost.com" ))
14
                    .build();
15
            employeeRepository.save(employee);
16
 
          
17
            TenantContext.clear();
18
        });
19
    }
20
 
          
21
}



Now we can run our application and test it. 


And we're done, hope this tutorial helps you understand what multi-tenancy is and how it's implemented in a Spring Boot project using MongoDB and Redis.

Full source code can be found on GitHub.

 

 

 

 

Top