Skip to main content

Multi-tenant application with Spring Boot + Spring Data JPA + Hibernate + MySQL + Thymeleaf

In this tutorial I will demonstrate how to create a multi-tenant  application with Spring BootSpring Data JPA , Hibernate and MySQL. I will also create a simple form using Thymeleaf

What you will need

  • Spring Tool Suite if you want to use the wizards or any other text editor or IDE will do (in this tutorial I am using STS 3.7.0)
  • JDK 1.8
  • Maven 3.0+ (note you can use Gradle as well if you wish but I will be using Maven)
Multi-tenancy

So what is software multi-tenancy? In general is the software architecture where one application instance serves multiple clients, something quite common in SaaS solutions . I will not go into details regarding the pros and cons of multi-tenant architectures, but there is really nice presentation on the subject here and here and probably the most popular article on the subject from Microsoft (a bit dated but still relevant).
With regards to implementation, at least in terms of data, there are 3 approaches:
  1. Separate databases: each tenant has its own database
  2. Separate schemas: tenants share common database where each tenant has its own set of tables (schema)
  3. Shared schema : data for all tenants is stored in the same tables and are identified through a tenant discriminator column.
Options 1 and 2 are probably the less intrusive for the application, provide the highest level of isolation and do not vary significant in terms of implementation.

How to identify the tenant


Another decision you need to take when implementing multi-tenancy is tenant identification.  This is the way to identify the tenant in the incoming requests. Again there is more than one approach with each having its pros cons. In general there are 3 ways:
  1. Custom http headers or OAuth 2.0 with bearer tokens
  2. Tenant ID in the URI (e.g. http://myservice.com/api/tenant_id/....)
  3. Tenant ID in the host name (e.g. http://tenantid.myservice.com)
I will be using option 2 where the tenat id is going to be part of the URI mainly to make the client simpler but option 1 with custom http headers would not vary significanlty. 

Nice post on the subject here.

Hibernate and Multi-tenancy


Hibernate currently supports the Separate Databases and Separate Schema approaches with support for the Discriminator option planned for version 5 of the API
We are going to use the separate database approach, with this approach Hibernate requires you to specify a MultiTenantConnectionProvider. This is an interface you need to implement and will allow Hibernate to obtain connections in a tenant specific manner. Hibernate also requires you to implement the interface CurrentTenantIdentifierResolver , as this is the contract for Hibernate to able to resolve what the application considers the current tenant identifier.

Creating a new project with STS

After this short introduction it is time to start STS and create a new Spring Starter Project from File -> New -> Spring Starter Project.
In the Dependencies step select JPA, MySQL, Thymeleaf and Web click Finish and STS should download your template and import it to your workspace.

Domain code


For the purpose of the tutorial we are going to create a very basic app for storing employee records. Imagine a scenario where each tenant would be a separate company storing their employees data in your SaaS based system.
To present an employee we need a domain class, a simple one will do for the purpose of our demo. We annotate our class with the relevant JPA annotations. As you can see below it is only a POJO that does not contain anything special with regards to multi-tenancy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.anakiou.mt.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 private Long id;
 private String firstName;
 private String lastName;
 private String department;
 private String office;

 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public String getDepartment() {
  return department;
 }

 public void setDepartment(String department) {
  this.department = department;
 }

 public String getOffice() {
  return office;
 }

 public void setOffice(String office) {
  this.office = office;
 }

}

Repository


Creating a Repository for our Employee class is trivial using Spring. For our basic CRUD operations we only need to create an interface that extends Spring's own CrudRepository. Again nothing specific needs to be done here to support multi-tenancy. 
Full code for EmployeeRepository.java is shown below:

1
2
3
4
5
6
7
8
9
package com.anakiou.mt.repository;

import org.springframework.data.repository.CrudRepository;

import com.anakiou.mt.domain.Employee;

public interface EmployeeRepository extends CrudRepository<Employee, Long>{

}

Multi-tenancy Interceptor


In order to identify the current identifier we will intercept the incoming request by using a handler interceptor. HandlerInterceptors are somehow similar to Servlet Filters. Spring provides an abstract class (HandlerInterceptorAdapter) which  contains a simplified implementation of the HandlerInterceptor interface for pre-only/post-only interceptors.
In our case we are only interested on intercepting the request before it reaches the controller in order to identify the tenant, so we will only override the preHandle method. The tenant ID is going to be a path variable and we are going to retrieve it and add it as an attribute to the incoming request before this is processed by the controller. Later you will how this is picked-up by our CurrentTenantIdentifierResolver.
Full code for the MultitenancyInterceptor.java is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.anakiou.mt.web;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class MultiTenancyInterceptor extends HandlerInterceptorAdapter {

 @Override
 public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler)
   throws Exception {
  Map<String, Object> pathVars = (Map<String, Object>) req.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
  
  if (pathVars.containsKey("tenantid")) {
   req.setAttribute("CURRENT_TENANT_IDENTIFIER", pathVars.get("tenantid"));
  }
  return true;
 }
}

CurrentTenantIdentifierResolverImpl

The implementation of the CurrentTenantIdentifierResolver is also pretty straightforward. We will retrieve the tenantid request attribute from Spring MVC RequestContextHolder and provide it to Hibernate (this is the tenantid we added in our interceptor).
Full code for the CurrentTenantidentifierResolverImpl.java is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.anakiou.mt.util;

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

@Component
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

 private static final String DEFAULT_TENANT_ID = "tenant_1";

 @Override
 public String resolveCurrentTenantIdentifier() {
  RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  if (requestAttributes != null) {
   String identifier = (String) requestAttributes.getAttribute("CURRENT_TENANT_IDENTIFIER",RequestAttributes.SCOPE_REQUEST);
   if (identifier != null) {
    return identifier;
   }
  }
  return DEFAULT_TENANT_ID;
 }

 @Override
 public boolean validateExistingCurrentSessions() {
  return true;
 }
}

DataSourceBasedMultiTenantConnectionProviderImpl

Our implementation of the MultiTenantConnectionProvider is also quite simple, we extend the AbstractDataSourceBasedMultiTenantConnectionProviderImpl provided by Hibernate, we let Spring inject our datasources, load them in a map  and resolve the datasource based on the tenantIdentifier.  
Full code for DataSourceBasedMultiTenantConnectionProviderImpl.java shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.anakiou.mt.util;

import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Component
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

 private static final long serialVersionUID = 8168907057647334460L;
 private static final String DEFAULT_TENANT_ID = "tenant_1";

 @Autowired
 private DataSource dataSource1;

 @Autowired
 private DataSource dataSource2;

 @Autowired
 private DataSource dataSource3;

 private Map<String, DataSource> map;

 @PostConstruct
 public void load() {
  map = new HashMap<>();
  map.put("tenant_1", dataSource1);
  map.put("tenant_2", dataSource2);
  map.put("tenant_3", dataSource3);
 }

 @Override
 protected DataSource selectAnyDataSource() {
  return map.get(DEFAULT_TENANT_ID);
 }

 @Override
 protected DataSource selectDataSource(String tenantIdentifier) {
  return map.get(tenantIdentifier);
 }
}

Configuration

In terms of configuration there are a couple of things we need to do. First we need to add our interceptor to  Spring's MVC  InterceptorRegistry. To do that  we extend the abstract class provided by Spring WebMvcConfigurerAdapter and override the addInterceptors method.
Full code for the WebMvcConfig.java is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.anakiou.mt.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.anakiou.mt.web.MultiTenancyInterceptor;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

 @Override
 public void addInterceptors(InterceptorRegistry registry) {
  registry.addInterceptor(new MultiTenancyInterceptor());
 }
}

It is probably a good idea to externalise the configuration for our datasources so we will use the @Configuration annotation and Spring will validate and bind the datasources from application.properties. For the demo we are going to define 3 datasources.
Full code for MultitenancyProperties.java is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.anakiou.mt.config;

import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties("spring.multitenancy")
public class MultitenancyProperties {

 @NestedConfigurationProperty
 private DataSourceProperties datasource1;

 @NestedConfigurationProperty
 private DataSourceProperties datasource2;

 @NestedConfigurationProperty
 private DataSourceProperties datasource3;

 public DataSourceProperties getDatasource1() {
  return datasource1;
 }

 public void setDatasource1(DataSourceProperties datasource1) {
  this.datasource1 = datasource1;
 }

 public DataSourceProperties getDatasource2() {
  return datasource2;
 }

 public void setDatasource2(DataSourceProperties datasource2) {
  this.datasource2 = datasource2;
 }

 public DataSourceProperties getDatasource3() {
  return datasource3;
 }

 public void setDatasource3(DataSourceProperties datasource3) {
  this.datasource3 = datasource3;
 }

}


Our application.properties, with the configuration for our 3 datasources should look similar to below (for running the application make sure MySQL is installed and running, the SQL script to create the employee table is available on GitHub):

spring.multitenancy.datasource1.url=jdbc:mysql://localhost:3306/tenant_1
spring.multitenancy.datasource1.username=user
spring.multitenancy.datasource1.password=pass
spring.multitenancy.datasource1.driver-class-name=com.mysql.jdbc.Driver

spring.multitenancy.datasource2.url=jdbc:mysql://localhost:3306/tenant_2
spring.multitenancy.datasource2.username=user
spring.multitenancy.datasource2.password=pass
spring.multitenancy.datasource2.driver-class-name=com.mysql.jdbc.Driver

spring.multitenancy.datasource3.url=jdbc:mysql://localhost:3306/tenant_3
spring.multitenancy.datasource3.username=user
spring.multitenancy.datasource3.password=pass
spring.multitenancy.datasource3.driver-class-name=com.mysql.jdbc.Driver

What also needs to be done is disable Spring's DataSourceAutoConfiguration and provide our multi-tenant DataSourceConfig. This will load the properties from MultitenancyProperties which in turn has been configured by application.properties and configure our datasources accordingly. To exclude DataSourceAutoConfiguration we parameterize the @SpringBootApplication as follows:

1
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

The implementation of DataSourceConfig.java should look something similar to below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.anakiou.mt.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceConfig {

 @Autowired
 private MultitenancyProperties multitenancyProperties;

 @Bean(name = { "dataSource", "dataSource1" })
 @ConfigurationProperties(prefix = "spring.multitenancy.datasource1")
 public DataSource dataSource1() {
  DataSourceBuilder factory = DataSourceBuilder
    .create(this.multitenancyProperties.getDatasource1().getClassLoader())
    .driverClassName(this.multitenancyProperties.getDatasource1().getDriverClassName())
    .username(this.multitenancyProperties.getDatasource1().getUsername())
    .password(this.multitenancyProperties.getDatasource1().getPassword())
    .url(this.multitenancyProperties.getDatasource1().getUrl());
  return factory.build();
 }

 @Bean(name = "dataSource2")
 @ConfigurationProperties(prefix = "spring.multitenancy.datasource2")
 public DataSource dataSource2() {
  DataSourceBuilder factory = DataSourceBuilder
    .create(this.multitenancyProperties.getDatasource2().getClassLoader())
    .driverClassName(this.multitenancyProperties.getDatasource2().getDriverClassName())
    .username(this.multitenancyProperties.getDatasource2().getUsername())
    .password(this.multitenancyProperties.getDatasource2().getPassword())
    .url(this.multitenancyProperties.getDatasource2().getUrl());
  return factory.build();
 }

 @Bean(name = "dataSource3")
 @ConfigurationProperties(prefix = "spring.multitenancy.datasource3")
 public DataSource dataSource3() {
  DataSourceBuilder factory = DataSourceBuilder
    .create(this.multitenancyProperties.getDatasource3().getClassLoader())
    .driverClassName(this.multitenancyProperties.getDatasource3().getDriverClassName())
    .username(this.multitenancyProperties.getDatasource3().getUsername())
    .password(this.multitenancyProperties.getDatasource3().getPassword())
    .url(this.multitenancyProperties.getDatasource3().getUrl());
  return factory.build();
 }
}

The last piece of configuration required is to configure Spring's JPA entity manager factory with the Hibernate specific properties for multitenancy, specifically:
  • Define the multi-tenancy strategy
  • Assign the MultiTenantConnectionProvider
  • Assign the MultiTenantIdentifierResolver
Full code for MultiTenancyJpaConfiguration.java shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.anakiou.mt.config;

import java.util.LinkedHashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.hibernate.MultiTenancyStrategy;
import org.hibernate.cfg.Environment;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

import com.anakiou.mt.domain.Employee;

@Configuration
@EnableConfigurationProperties(JpaProperties.class)
public class MultiTenancyJpaConfiguration {

 @Autowired
 private DataSource dataSource;

 @Autowired
 private JpaProperties jpaProperties;

 @Autowired
 private MultiTenantConnectionProvider multiTenantConnectionProvider;

 @Autowired
 private CurrentTenantIdentifierResolver currentTenantIdentifierResolver;

 @Bean
 public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
  Map<String, Object> hibernateProps = new LinkedHashMap<>();
  hibernateProps.putAll(jpaProperties.getHibernateProperties(dataSource));

  hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
  hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
  hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
  hibernateProps.put(Environment.DIALECT, "org.hibernate.dialect.MySQLDialect");

  return builder.dataSource(dataSource).packages(Employee.class.getPackage().getName()).properties(hibernateProps).jta(false).build();
 }
}

Controllers and Views with Thymeleaf

Now that our backend is pretty much ready let's create a couple of controllers to handle the index page and a form to add employees to our application. 
IndexController.java is as simple as it gets:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.anakiou.mt.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class IndexController {

 @RequestMapping
 public String index() {
  return "index";
 }
}

and the corresponding Thymeleaf template, notice the tenant_id path variables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Welcome to our multi-tenant employees app</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</head>
<body>
 <div class="container">
  <div class="jumbotron">
   <h1>Welcome to our multi-tenant app</h1>
   <p>Use the links below to navigate to the different tenants</p>
  </div>
  <div class="row">
   <div class="col-sm-4">
    <h3>
     <a th:href="@{/tenant_1}">Tenant 1</a>
    </h3>
    <p>...</p>
   </div>
   <div class="col-sm-4">
    <h3>
     <a th:href="@{/tenant_2}">Tenant 2</a>
    </h3>
    <p>...</p>
   </div>
   <div class="col-sm-4">
    <h3>
     <a th:href="@{/tenant_3}">Tenant 3</a>
    </h3>
    <p>...</p>
   </div>
  </div>
 </div>
</body>
</html>

Next we create the controller which is going to handle the requests for adding a new employee and retrieving the list of the existing employees for the current tenant. You will notice that in the employees method we have included the tenantid path variable, this is not required and it is there so that we can reference it in our html. We also add an Employee instance to our model for the form.
The addEmployee method is also very simple simple, saves the employee instance to our database and redirects to the current page. 
Full code for EmployeeController.java shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.anakiou.mt.web;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.anakiou.mt.domain.Employee;
import com.anakiou.mt.repository.EmployeeRepository;

@Controller
@RequestMapping("/{tenantid}")
public class EmployeeController {

 @Autowired
 private EmployeeRepository employeeRepository;

 @PersistenceContext
 private EntityManager entityManager;

 @RequestMapping
 public String employees(@PathVariable String tenantid, Model model) {
  model.addAttribute("tenantid", tenantid);
  model.addAttribute("employee", new Employee());
  model.addAttribute("employees", employeeRepository.findAll());
  return "employees";
 }

 @RequestMapping(value = "/add", method = RequestMethod.POST)
 @Transactional
 public String addEmployee(@ModelAttribute Employee employee, Model model) {
  employeeRepository.save(employee);
  return "redirect:/{tenantid}";
 }
}

and the respective employees.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Employee List</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</head>
<body>
 <div class="container">
  <div>
   <h1 class="page-header" th:text="${tenantid}">Tenant</h1>
  </div>
  
  <div>
  <h2>Add a new employee</h2>
   <form role="form" th:action="@{/{tenantid}/add(tenantid=${tenantid})}" th:object="${employee}" method="post">
    <div class="form-group">
     <label for="first_name">First Name:</label> 
     <input type="text" class="form-control" id="first_name" th:field="*{firstName}" />
    </div>
    <div class="form-group">
     <label for="last_name">Last Name:</label> 
     <input type="text" class="form-control" id="last_name" th:field="*{lastName}" />
    </div>
    <div class="form-group">
     <label for="department">Department:</label> 
     <input type="text" class="form-control" id="department" th:field="*{department}" />
    </div>
    <div class="form-group">
     <label for="office">Office:</label> 
     <input type="text" class="form-control" id="office" th:field="*{office}" />
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
   </form>
  </div>

  <div>
   <h2>Employees List</h2>
   <table class="table table-bordered">
    <thead>
     <tr>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Department</th>
      <th>Office</th>
     </tr>
    </thead>
    <tbody>
     <tr th:each="employee : ${employees}">
      <td th:text="${employee.firstName}"></td>
      <td th:text="${employee.lastName}"></td>
      <td th:text="${employee.department}"></td>
      <td th:text="${employee.office}"></td>
     </tr>
    </tbody>
   </table>
  </div>
 </div>
</body>
</html>

Running the multi-tenant app

All the parts of our application are ready and we can run it by selecting MultitenancyApplication.java , right-click Run As -> Spring Boot App . If you now navigate to http://localhost:8080/  you should get something similar to below:



Clicking any of the tenant links navigates to the form to add  a new employee. Notice the tenant identifier in the URI and the html.


We can try out our form to add a new employee to the current tenant. 


If everything works well clicking the Submit should add the employee to the database of the current tenant.

You can confirm this by navigating again to the form using a different tenant identifier.

Similarly for the third tenant

Let's add some more employees to tenant 3 


You can again check that these have been added to the correct database by using the different tenant identifiers.

Enjoy!

Get the source code

Source code is available on GitHub

Comments

  1. Very nice tutorial. If i have all 3 tenant information in default database instead of application.properties file, how can i make the database connection?

    ReplyDelete
    Replies
    1. Hi,

      I am not sure what you mean by this.

      Do you mean one database with different schemas or different tables?

      Please give more information.

      Thanks

      Delete
  2. I asked about dynamic datasource in separate databases approach. For ex, I have many database(default_db,tenant_1,tenant_2,tenant_3,tenant_4, etc....). I stored database name(tenant_1,tenant_2,tenant_3,tenant_4, etc....), username and password in one table on default_db.

    ReplyDelete
    Replies
    1. Hi,
      I know it's an old post but I have the same problem and would like to ask if you've found a way to keep tenants information in a default database?
      Thanks a lot.

      Delete
  3. Very very interesting article. Thanks for sharing

    ReplyDelete
  4. Great post! I was able to get this working.

    However, when I implemented multi-threading using ExecutorService - the new threads seemed to be getting the default values of the tenant identifier- for eg. tenant_1 in this case even though the api call was made using tenant_2.

    I am using ExecutorService for a bunch for inserts that happen in a multi-threaded manner - each insert being a new transaction. However, this fails with multi-tenancy. Anyone aware of this issue?

    ReplyDelete
  5. This article was very helpful to me. Thank you for your work.

    ReplyDelete
  6. the given example is not working

    ReplyDelete
  7. Hello, I do I make the mulitenant app as a subdomain e.g. tenant1.app.com, tenant2.app.com ?

    ReplyDelete
  8. I have a multi database architecture,
    where I have 2 databases per customer, and 2 other databases used by all customers.
    Do you have an idea to manage this architecture with hibernate and spring?
    Thank you in advance.

    ReplyDelete

Post a Comment