Tuesday, May 24, 2016

Multitenancy with Spring Boot

For those who are not familiar with the term Multitenancy, let me start answering some questions that may be come to your mind.

What is multitenancy?

It is a software architecture where the same instance of the software serves multiple tenants. You can see a tenant as an organization. For example, if I have a software platform to sell TV's, my tenants could be Samsung, Philips and Sony. The good thing is that with this approach you are sharing resources, and if you work with cloud platforms this could be a good idea. On the other hand, as you know nothing is free, you have to be aware about the security and the isolation of the data and other things. Before you decide if this kind architecture matches with your requirements, you have to study in deep your requirements.

How to implement that?

I will not delve into the details that motivate the choice of a strategy or another. There are three approaches to do that:
  • Separate database per tenant (picture 2).
  • Separate schema per tenant.
  • Share database and schema.

Ok, we are ready to start. In this example, I am going to implement the first option (separata database per tenant). Note that in this example, the tenants will be added dynamically, this means that only adding the configuration of the tenant in the application.yml will be enough for the application be aware of the new tenant.

I remember you, that this example is using Spring Boot and Hibernate.

First of all we have to add in the application.yml the multitenancy configuration we want. In this case, I have two tenants.

multitenancy:
tenants:
-
name: tenant_1
default: true
url: jdbc:mysql://localhost:3306/tenant_1?serverTimezone=UTC
username: user
password: pass
driver-class-name: com.mysql.jdbc.Driver
-
name: tenant_2
default: false
url: jdbc:mysql://localhost:3306/tenant_2?serverTimezone=UTC
username: user
password: pass
driver-class-name: com.mysql.jdbc.Driver
view raw application.yml hosted with ❤ by GitHub
Also we have to exclude the default data source configuration that provides Spring Boot.

// we have to exclude the default configuration, because we want to provide
// our multi tenant data source configuration.
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
view raw App.java hosted with ❤ by GitHub
The next step, is to provide a mechanism to determine, in runtime, which tenant is accessing to the application instance. To do that, Spring provides an interface to implement it.

@Component
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
return RequestContextHolderUtils.getCurrentTenantIdentifier();
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
RequestContextHolderUtil is a class which determines the tenant based on a pattern contained in the Uri of the request.

public class RequestContextHolderUtils {
private static final String DEFAULT_TENANT_ID = "tenant_1";
public static final String getCurrentTenantIdentifier() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
String identifier = (String) requestAttributes.getAttribute(CustomRequestAttributes.CURRENT_TENANT_IDENTIFIER,RequestAttributes.SCOPE_REQUEST);
if (identifier != null) {
return identifier;
}
}
return DEFAULT_TENANT_ID;
}
}
Now we need a mechanism to select the correct database (DataSource object) based on the current tenant which is accessing to the application. Once you have the tenant id (above step), you only have to extend the class AbstractDataSourceBasedMultiTenantConnectionProviderImpl provided by Spring.

public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
private static final long serialVersionUID = 1L;
private String defaultTenant;
private Map<String, DataSource> map;
public DataSourceBasedMultiTenantConnectionProviderImpl(String defaultTenant, Map<String, DataSource> map) {
super();
this.defaultTenant = defaultTenant;
this.map = map;
}
@Override
protected DataSource selectAnyDataSource() {
return map.get(defaultTenant);
}
@Override
protected DataSource selectDataSource(String tenantIdentifier) {
return map.get(tenantIdentifier);
}
public DataSource getDefaultDataSource() {
return map.get(defaultTenant);
}
}
Note that the object Map (attribute of class AbstractDataSourceBasedMultiTenantConnectionProviderImpl) is configured when the Spring context is loaded reading the multitenancy properties from application.yml. This configuration is performed in a class annotated with @Configuration. You can see the code below.

@Configuration
@EnableConfigurationProperties(MultitenancyConfigurationProperties.class)
public class MultitenancyConfiguration {
@Autowired
private MultitenancyConfigurationProperties multitenancyProperties;
@Bean(name = "multitenantProvider")
public DataSourceBasedMultiTenantConnectionProviderImpl dataSourceBasedMultiTenantConnectionProvider() {
HashMap<String, DataSource> dataSources = new HashMap<String, DataSource>();
multitenancyProperties.getTenants().stream().forEach(tc -> dataSources.put(tc.getName(), DataSourceBuilder
.create()
.driverClassName(tc.getDriverClassName())
.username(tc.getUsername())
.password(tc.getPassword())
.url(tc.getUrl()).build()));
return new DataSourceBasedMultiTenantConnectionProviderImpl(multitenancyProperties.getDefaultTenant().getName(), dataSources);
}
@Bean
@DependsOn("multitenantProvider")
public DataSource defaultDataSource() {
return dataSourceBasedMultiTenantConnectionProvider().getDefaultDataSource();
}
}

The last step is to implement a Rest controller to access to the data. Note that the Uri of the endpoint contains the 'tenantId' pattern. Of course, there are different ways to do that. For example using domains or subdomains and so on.

@RestController
@RequestMapping("/{tenantId}/invoice")
public class InvoiceController {
@Autowired
private InvoiceRepository invoiceRepository;
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public List<Invoice> invoices() {
return Lists.newArrayList(invoiceRepository.findAll());
}
}
Finally, you can find all the implementation in my GitHub profile here


Feel free to contact me for questions or any doubt.

Happy codying

4 comments:

  1. Hi,

    How to manage connection pooling in this kind of sscenario?

    ReplyDelete
  2. Thanks a lot. The issue I have is that JPA is only creating tables in the default datasource's database and does not in all the other tenants. Is this expected and is there a way to make JPA create tables in all tenants?

    ReplyDelete
  3. HOw to get the informtion of all datasources from database

    ReplyDelete