# Multi-tenancy Using Spring Boot - Shared Schema Subdomain Identifier Architecture

## Introduction :

Multi-tenancy means that multiple customers of an application are using the same codebase and server but with different databases. Despite the fact that they share resources, customers aren't aware of each other, and their data is kept totally separate and safe.


## Use Cases:

Let's understand the use cases in a simpler way, let's assume we have developed an application that is configured with a domain `geekyapp.com`.

For example, we have three clients who use our application:

- Fireclient
- Waterclient
- Airclient

Now, let's say every client wants to have their own schema which will be separate from another client because of privacy concerns of their data. In such cases, we can go for a multi-tenancy approach and provide them with different URLs like:

- fireclient.geekyapp.com
- waterclient.geekyapp.com
- airclient.geekyapp.com

## Technologies & Plugins used:

- Spring Boot
- FlyWay
- JPA & Hibernate
- MySQL

## Code Source :
POJOs and Repositories source codes can be found below GitHub repos:

- GitHub - [`BASE-SERVICE`](https://github.com/geekdib/mt-base-service)
- GitHub - [`CLIENT-SERVICE`](https://github.com/geekdib/mt-client-service)

Let's dive into code and development part. We will develop two different services for this topic which are listed below:

### Base-Service
It will be responsible for creating new schemas dynamically when a new client requests registration and also this service will provide all required details to client service through API calls.

### Client-Service
It will be responsible for performing business operations for each client and it will invoke `Base-Service` for runtime database creation for new clients.


## Base-Service

### Dependencies

As you can find out we have used org.flywaydb dependencies, which provides us the option for creating new schemas for customers in runtime.

```
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-maven-plugin</artifactId>
			<version>8.5.7</version>
		</dependency>
		<dependency>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-mysql</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
	</dependencies>
```

### DB Structure SQL File

Inside directory `/mt-base-service/src/main/resources/db/migration` we have placed `V1_0__client.sql` files which contains our DB schema structure. Flyway will use this SQL file for creating new tenant databases in runtime. 

> Note: We need to keep the naming convention of this file in the required format.

Here, `V1_0__ ` will be the version of our schema structure, and `client.sql` contains any random name.

```
DROP TABLE IF EXISTS `product`;

CREATE TABLE `product` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
``` 

### Main Class

Here we have just created and returned a bean of `FlywayMigrationStrategy`.

```
@SpringBootApplication
public class MtBaseServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(MtBaseServiceApplication.class, args);
	}
	
	@Bean
	public FlywayMigrationStrategy flywayMigrationStrategy() {
		return flyway -> {
			// do nothing
		};
	}
}
``` 

### BaseRestController.java

The `/addsource/{tenant}` mapping method will receive an `OrgDTO` as a request from client service which contains the required information for new tenant creation. Later, we are saving `OrgDTO` in the base database and also we are creating one `Dsource` object and saving it in the database for future needs.

The `/getAll` mapping method will return a response of all `Dsource` objects stored in database so that Client-Service can use these objects to create and initialize `Spring Datasource` objects while startup.

The `fly.migrate();` function tells `Fylway` to create a database with below given configurations.

```
Flyway fly = Flyway.configure()                                               
       .configuration(flyway.getConfiguration())                 
       .schemas(tenant)   //here we are passing the tenant name which will be the schema name.                                   
       .defaultSchema(tenant)   //Flyway will consider the same as default schema.                                   
       .load();
```

```
@RestController
@RequestMapping("/base")
public class BaseRestController {
	
	@Autowired
	private DsourceRepository dsourceRepository;
	
	@Autowired
	private OrgEntityRepository orgEntityRepository;
	
	@Autowired                                                            
	private Flyway flyway;
	
	@CrossOrigin(origins = "*")
	@PostMapping("/addsource/{tenant}")
	public ResponseEntity<Object> addDSource(@RequestBody OrgDTO orgDTO, @PathVariable("tenant") String tenant) {
		
		Dsource dsource = new Dsource();
		dsource.setTenantId(tenant);
		dsource.setUsername("root");
		dsource.setPassword("WitchMYSQL");
		dsource.setUrl("jdbc:mysql://localhost:3306/"+tenant+"?useSSL=false");
		dsource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dsourceRepository.save(dsource);
		
		OrgEntity orgEntity = new OrgEntity();
		orgEntity.setEmail(orgDTO.getEmail());
		orgEntity.setOrgName(orgDTO.getOrgName());
		orgEntity.setFirstName(orgDTO.getFirstName());
		orgEntity.setLastName(orgDTO.getLastName());
		orgEntity.setInstanceName(orgDTO.getInstanceName());
		orgEntityRepository.save(orgEntity);
		
		Flyway fly = Flyway.configure()                                                
        .configuration(flyway.getConfiguration())                 
        .schemas(tenant)                                          
        .defaultSchema(tenant)                                    
        .load();
		
		fly.migrate();
		
		return new ResponseEntity<>("success", HttpStatus.OK);
	}
	
	@CrossOrigin(origins = "*")
	@GetMapping("/getAll")
	public List<Dsource> getAll() {
		return dsourceRepository.findAll();
	}
}
``` 
That's it, we are all set with Base-Service which will accept tenant creation requests from our Client-Service and it will dynamically create a DB schema for new tenants. Let's go ahead and develop our Client-Service.

## Client-Service

### Dependencies

```
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
			<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>
``` 

### Configurations - Important

#### `CurrentTenantIdentifierResolverImpl.java`

Here we are resolving the current tenant ID from `TenantContextHolder` which is a ThreadLocal component. The `DEFAULT_TENANT_ID` will be having a mandatory database name so that `Spring Boot` application can start without any issues, else because of Auto Configuration nature of `Spring Boot` application could get crashed while startup.

```
@Component
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    private static final String DEFAULT_TENANT_ID = "test1";

    @Override
    public String resolveCurrentTenantIdentifier() {
    			String tenant = TenantContextHolder.getTenant();
                return tenant!=null && !tenant.equalsIgnoreCase("") ? tenant : DEFAULT_TENANT_ID;
    }

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

}

``` 
#### `DataSourceBasedMultiTenantConnectionProviderImpl.java`

This component does its job by iteration through all data sources, if we will use method `selectDataSource(String tenantIdentifier)` it will return us the Datasource object by performing a lookup by using tenant ID.

```
@Component
public class DataSourceBasedMultiTenantConnectionProviderImpl
extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

	private static final long serialVersionUID = 1L;

	@Autowired
	private Map<String, DataSource> dataSourcesMtApp;

	@Override
	protected DataSource selectAnyDataSource() {
		return this.dataSourcesMtApp.values().iterator().next();
	}

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

``` 
#### `MultiTenancyJpaConfiguration.java`

In this component, we will invoke Base-Service's `/getAll REST` endpoint for getting all stored `Dsource` objects in DB. Also we are having some custom bean methods for `Entity Manager` and `Transaction Manager`.

Next, we are iterating each `Dsource` object received from Base-Service and we are creating Spring Datasource objects by using `Dsource`'s information.

```
DataSourceBuilder<?> factory = DataSourceBuilder.create().url(source.getUrl())
					.username(source.getUsername()).password(source.getPassword())
					.driverClassName(source.getDriverClassName());
``` 

```
@Configuration
@EnableTransactionManagement
@ComponentScan("com.itdib")
@Lazy
public class MultiTenancyJpaConfiguration {

	@Autowired
	private JpaProperties jpaProperties;

	@Autowired
	private RestTemplate rs;

	@Primary
	@Bean(name = "dataSourcesMtApp")
	public Map<String, DataSource> dataSourcesMtApp() {

		HttpHeaders headers = new HttpHeaders();

		ResponseEntity<Dsource[]> response =
				rs.exchange(
						"http://localhost:2021/base/getAll", HttpMethod.GET, new HttpEntity<Object>(headers),
						Dsource[].class);

		Dsource[] dsList = response.getBody();
		
		Map<String, DataSource> result = new HashMap<>();
		for (Dsource source :dsList) {
			DataSourceBuilder<?> factory = DataSourceBuilder.create().url(source.getUrl())
					.username(source.getUsername()).password(source.getPassword())
					.driverClassName(source.getDriverClassName());
			
			HikariDataSource ds = (HikariDataSource)factory.build();
	        ds.setKeepaliveTime(40000);
	        ds.setMinimumIdle(1);
	        ds.setMaxLifetime(45000);
	        ds.setIdleTimeout(35000);
			result.put(source.getTenantId(), ds);
		}
		
		return result;
	}

	@Bean
	public MultiTenantConnectionProvider multiTenantConnectionProvider() {
		return new DataSourceBasedMultiTenantConnectionProviderImpl();
	}

	@Bean
	public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
		return new CurrentTenantIdentifierResolverImpl();
	}

	@Bean(name="entityManagerFactoryBean")
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
			AbstractDataSourceBasedMultiTenantConnectionProviderImpl multiTenantConnectionProvider,
			CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {

		Map<String, Object> hibernateProps = new LinkedHashMap<>();
		hibernateProps.putAll(this.jpaProperties.getProperties());
		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("hibernate.hbm2ddl.auto", "update");
		hibernateProps.put("hibernate.dialact", "org.hibernate.dialect.MySQL8Dialect");
		LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
		result.setPackagesToScan("com.itdib");
		result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
		result.setJpaPropertyMap(hibernateProps);

		return result;
	}

	@Bean
	@Primary
	public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
		return entityManagerFactoryBean.getObject();
	}

	@Bean
	public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
		return new JpaTransactionManager(entityManagerFactory);
	}
}
``` 
### TenantContextHolder

This component is basically meant for handling the current tenant ID throughout the request execution so that Hibernate can pick the correct requested data source according to the tenant ID received as a subdomain.

It really plays a very important role in our architecture because keeping a specific tenant ID for users helps Hibernate pick the correct Datasource from the pool.

```
public class TenantContextHolder {

    private static final ThreadLocal<String> CONTEXT = new InheritableThreadLocal <>();

    public static void setTenantId(String tenant) {
        CONTEXT.set(tenant);
    }

    public static String getTenant() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}
``` 

### `ClientServiceImpl.java`

This component holds our business logic as well as `register(OrgDTO orgDTO)` method will take care of invoking Base-Service for creating database schemas dynamically whenever the user registers a new tenant.

Also we are creating a Datasource object and giving it to `DataSourcesMtApp` so that it will include this new Datasource in the existing Datasource collection. Otherwise, our application will not be able to make DB connection for this newly registered tenant because in `MultiTenancyJpaConfiguration.java` we call `BASE-SERVICE/getAll` API only while Application Startup.

```
DataSourceBuilder<?> factory = DataSourceBuilder.create(MultiTenancyJpaConfiguration.class.getClassLoader())
	.url("jdbc:mysql://localhost:3306/" + orgDTO.getInstanceName() + "?useSSL=false").username("root")
	.password("WitchMYSQL").driverClassName("com.mysql.cj.jdbc.Driver");
HikariDataSource ds = (HikariDataSource) factory.build();
ds.setKeepaliveTime(40000);
ds.setMinimumIdle(1);
ds.setMaxLifetime(45000);
ds.setIdleTimeout(35000);
dataSourcesMtApp.put(orgDTO.getInstanceName(), ds);
``` 


```
@Service
public class ClientServiceImpl implements ClientService {

	@Autowired
	private RestTemplate restTemplate;

	@Autowired
	private Map<String, DataSource> dataSourcesMtApp;

	@Override
	public String register(OrgDTO orgDTO) {
		boolean flag = false;
		HttpHeaders headers = new HttpHeaders();
		ResponseEntity<Dsource[]> response = restTemplate.exchange("http://localhost:2021/base/getAll", HttpMethod.GET,
				new HttpEntity<Object>(headers), Dsource[].class);

		if (response.getBody() != null) {
			Dsource[] dsList = response.getBody();
			if (dsList != null && dsList.length > 0) {
				for (Dsource d : dsList) {
					if (d.getTenantId().equalsIgnoreCase(orgDTO.getInstanceName())) {
						flag = true;
					}
				}
			}
		}

		if (flag) {
			return "tenant already exists!";
		}

		String url = "http://localhost:2021/base/addsource/" + orgDTO.getInstanceName();
		try {
			restTemplate.postForEntity(url, orgDTO, String.class);
		} catch (Exception e) {
		}

		DataSourceBuilder<?> factory = DataSourceBuilder.create(MultiTenancyJpaConfiguration.class.getClassLoader())
				.url("jdbc:mysql://localhost:3306/" + orgDTO.getInstanceName() + "?useSSL=false").username("root")
				.password("WitchMYSQL").driverClassName("com.mysql.cj.jdbc.Driver");
		HikariDataSource ds = (HikariDataSource) factory.build();
		ds.setKeepaliveTime(40000);
		ds.setMinimumIdle(1);
		ds.setMaxLifetime(45000);
		ds.setIdleTimeout(35000);
		dataSourcesMtApp.put(orgDTO.getInstanceName(), ds);
		TenantContextHolder.setTenantId(orgDTO.getInstanceName());
		return "tenant registered successfully!";
	}

}
``` 

That's it we are all set with both service development, it's time to go ahead and test both.

First, let's hit `CLIENT-SERVICE`'s `/registerOrg` REST endpoint with below request body. This will internally invoke `BASE-SERVICE`'s `/base/addsource/{tenant}` REST endpoint to create a new DB schema for our tenant.

```
{
    "email":"test@test.com",
    "orgName":"fireclient",
    "firstName":"fire",
    "lastName":"client",
    "instanceName":"fireclient"
}
``` 
Below is the excepted response,

![Screenshot 2022-08-04 at 10.11.42 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1659631327293/Y-fLEZqv5.png align="left")

Let's hit the same API for the second time with different request bodies for the second tenant.

```
{
    "email":"test2@test.com",
    "orgName":"airclient",
    "firstName":"air",
    "lastName":"client",
    "instanceName":"airclient"
}
``` 
Below is the expected response,
![Screenshot 2022-08-04 at 10.14.42 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1659631505596/hnEBGW1u6.png align="left")

Now, let's insert some test data in both tenants databases created by Flyway.


```
For fireclient,

INSERT INTO `fireclient`.`product` (`id`, `name`) VALUES ('1', 'fire1');
INSERT INTO `fireclient`.`product` (`id`, `name`) VALUES ('2', 'fire2');

For airclient,

INSERT INTO `airclient`.`product` (`id`, `name`) VALUES ('1', 'air1');
INSERT INTO `airclient`.`product` (`id`, `name`) VALUES ('2', 'air2'); 

``` 

Finally, let's hit `CLIENT-SERVICE`'s `/getProducts` REST endpoint to test if our application connects to correct database and gives us expected output.

> Note: In [`this`](http://tenant.localhost:2020/getProducts) tenant should be our tenant names like `airclient` and `fireclient`.

Response for [`this`](http://airclient.localhost:2020/getProducts) **POST** call,

![Screenshot 2022-08-04 at 10.21.12 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1659631888565/yyCtvlQ6B.png align="left")

Response for [`this`](http://fireclient.localhost:2020/getProducts) **POST** call,

![Screenshot 2022-08-04 at 10.21.42 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1659631948752/SLhc-l7GL.png align="left")

We can clearly see our application does its job well and gives us a list of Products for the requested tenant by dynamically switching between different tenant databases.

Thanks for reading this article, Happy coding!!!








