DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Spring Beans With Auto-Generated Implementations: How-To
  • MongoDB With Spring Boot: A Simple CRUD
  • Spring Microservice Tip: Abstracting the Database Hostname With Environment Variable

Trending

  • How to Build Real-Time BI Systems: Architecture, Code, and Best Practices
  • Rust, WASM, and Edge: Next-Level Performance
  • Endpoint Security Controls: Designing a Secure Endpoint Architecture, Part 2
  • Efficient API Communication With Spring WebClient
  1. DZone
  2. Data Engineering
  3. Databases
  4. Multi-Tenancy Implementation Using Spring Boot, MongoDB, and Redis

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.

By 
Anicet Eric user avatar
Anicet Eric
·
Dec. 19, 20 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
34.0K Views

Join the DZone community and get the full member experience.

Join For Free

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

Prerequisites

  • Spring Boot 2.4
  • Maven 3.6.+
  • JAVA 8+
  • Mongo 4.4
  • Redis 5

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.

Spring Framework Database connection Spring Boot Redis (company) MongoDB Java (programming language) Implementation

Opinions expressed by DZone contributors are their own.

Related

  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Spring Beans With Auto-Generated Implementations: How-To
  • MongoDB With Spring Boot: A Simple CRUD
  • Spring Microservice Tip: Abstracting the Database Hostname With Environment Variable

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!