Chuyển tới nội dung
Home » Multi Tenant Java Web Application | Maven Dependencies

Multi Tenant Java Web Application | Maven Dependencies

Spring Tips: Multitenant JDBC

Data Isolation

In an on-premise solution or a managed solution, there exists a dedicated instance of the database for each tenant. But when the product is multi-tenant, the same database can be shared across different tenants. This introduces the complexity of preventing one tenant from accessing the data of another tenant. Data Isolation can be architected in different ways:

  • Add a Company Id to all the tables and qualify all queries with a company id.
  • Maintain different databases for different tenants and connect to the correct database at runtime.
  • Maintain a single database, but different tables for different tenants qualified by the Company Id

A comparison of these three approaches can be seen below.

Feature Add Company Id Separate Database Separate Tables
Data Customization Not Possible, since only a single definition of table exists for all customers. Possible since a single table definition exist for each customer. Possible since a single table definition exists for each customer.
Security Very Low. Independent developers need to ensure that they add the correct clause to the query. It is possible to create views to mimic the separate table design. Very high. Since it does not depend on individual developers to access the correct data. High. With standardized table names, libraries can be added to access correct tables. If correct table is not accessed, no data is available.
Inter-dependency and performance The data size of one customer affects the performance of other customers Every customer is affected only by their data size. Every customer is affected only by their data size.
Scalable Model Partially scalable. Only one database has to be maintained and configured. But as the number of customers grow, the table sizes grow. Not Scalable, as the number of customers increase the number of databases to be maintained grows. Scalable, Only one database has to be maintained, and as customers grow, the number of tables in a database grows.
Customer On-boarding Easy. No database related tasks need to be done. All tables and columns exist for all customers. Difficult. A database has to be created and setup. Tables can be created based on what is required. Medium, Tables have to be created. These can be created on the first access of the table reducing effort.

Database Approach

In this section, we’ll implement multi-tenancy based on a Database per Tenant model.

5.Tenants Declaration

We have multiple databases in this approach, so we need to declare multiple data sources in the Spring Boot application. We can configure the DataSources in separate tenant files. So, we create the tenant_1.properties file in allTenants directory and declare the tenant’s data source:


name=tenant_1
datasource.url=jdbc:postgresql://localhost:5432/tenant1
datasource.username=postgres
datasource.password=123456
datasource.driver-class-name=org.postgresql.Driver

Moreover, we create a tenant_2.properties file for another tenant:


name=tenant_2
datasource.url=jdbc:postgresql://localhost:5432/tenant2
datasource.username=postgres
datasource.password=123456
datasource.driver-class-name=org.postgresql.Driver

We will end up with a file for each tenant:

5.DataSource Declaration

Now we need to read the tenant’s data and create DataSource using the DataSourceBuilder class. Also, we set DataSources in the AbstractRoutingDataSource class. Let’s add a MultitenantConfiguration class for that:


@Configuration
public class MultitenantConfiguration {
@Value("${defaultTenant}")
private String defaultTenant;
@Bean
@ConfigurationProperties(prefix = "tenants")
public DataSource dataSource() {
File[] files = Paths.get("allTenants").toFile().listFiles();
Map

resolvedDataSources = new HashMap<>();
for (File propertyFile : files) {
Properties tenantProperties = new Properties();
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
try {
tenantProperties.load(new FileInputStream(propertyFile));
String tenantId = tenantProperties.getProperty("name");
dataSourceBuilder.driverClassName(tenantProperties.getProperty("datasource.driver-class-name"));
dataSourceBuilder.username(tenantProperties.getProperty("datasource.username"));
dataSourceBuilder.password(tenantProperties.getProperty("datasource.password"));
dataSourceBuilder.url(tenantProperties.getProperty("datasource.url"));
resolvedDataSources.put(tenantId, dataSourceBuilder.build());
} catch (IOException exp) {
throw new RuntimeException("Problem in tenant datasource:" + exp);
}
}
AbstractRoutingDataSource dataSource = new MultitenantDataSource();
dataSource.setDefaultTargetDataSource(resolvedDataSources.get(defaultTenant));
dataSource.setTargetDataSources(resolvedDataSources);
dataSource.afterPropertiesSet();
return dataSource;
}
}

First, we read the tenants’ definitions from allTenants directory and create the DataSource bean using the DataSourceBuilder class. After that, we need to set a default data source and target source for the MultitenantDataSource class to connect to using setDefaultTargetDataSource and setTargetDataSources, respectively. We set one of the tenant’s names as a default data source from the application.properties file using defaultTenant attribute. To finalize the initialization of the data source, we call the afterPropertiesSet() method. Now that our setup is ready.

Spring Tips: Multitenant JDBC
Spring Tips: Multitenant JDBC

About the author

André is a versatile and talented developer with 10+ years of industry experience. He is skilled at Java, Java EE, JavaScript, and more.

Years of Experience

17

Dynamic-Multi-Tenancy-Using-Java-Spring-Boot-Security-JWT-Rest-API-MySQL-Postgresql-full-example

I wanted a solution where multi-tenancy is achieved by having a database per tenant and all user information (username, password, client Id etc) for authentication and authorization stored in a user table in the respective tenant databases. It meant that not only did I need a multi-tenant application, but also a secure application like any other web application secured by Spring Security. I know how to use Spring Security to secure a web application and how to use Hibernate to connect to a database. The requirement further dictated that all users belonging to a tenant be stored in the tenant database and not a separate or central database. This would allow for complete data isolation for each tenant.

Goal:


Achive Application SaaS Model client wise different database. You can connet multiple schema with single database. You can connect multiple database.

What is multi-tenancy ?

Multi-tenancy is an architecture in which a single instance of a software application serves multiple customers. Each client is called a tenant. Tenants may be given the ability to customize some parts of the application.

A multi-tenant application is where a tenant (i.e. users in a company) feels that the application has been created and deployed for them. In reality, there are many such tenants and they too are using the same application but get a feel that its built just for them.

Technology and Project Structure :


Java 11 Spring Boot Spring Security Spring AOP Spring Data JPA Hibernate JWT MySQL, PostgreSQL IntliJ

You can do that by using : https://start.spring.io/

Need more instruction? Please follow the article:https://dzone.com/articles/dynamic-multi-tenancy-using-java-spring-boot-sprin

Architecting a Multi-tenant Application

A multi-tenant application is an application where a single running instance serves many customers. An alternative to multi-tenancy is managed services, where one running instance is set up for each customer. The table below shows a comparison between the two approaches.

Feature Multi-Tenant Application Managed Services
Cost Structure Supports usage based pricing as well as tiered or flat pricing. Can support only tiered or flat pricing.
Resources Shared Resources. Resource is allocated based on the actual load for a tenant. Dedicated resources. Resource is allocated based on perceived load for the tenant and cannot be reassigned to other tenants automatically.
Operation and Maintenance Manage and administer a single instance for a number of customers Manage and administer as many instances as customers
Scalable Model Scalable, since a number of customers are serviced by one instance Not Scalable. As the number of customers increase the maintenance requirements increases proportionally.

For SaaS products it can be seen that there are a number of benefits if the product is a multi-tenant product. But for an application to benefit by multi-tenancy, it’s architecture should take care of:

  • Data Isolation
  • Feature Customization
  • Execution Environment Isolation

Styling and customization

A quick and easy way to give tenants their own customizable style would be to do something like this:


@RestController @RequestMapping("/style") public class StyleController { @GetMapping(value = "tenantStyle.css", produces = {"text/css"}) public String getTenantStyle(@Tenant String tenant) { if (tenant == null) { return "body { background-color: #fcd2d2; }"; } else if (tenant.equals("tenant1")) { return "body { background-color: #ebebfc; }"; } else if (tenant.equals("tenant2")) { return "body { background-color: #edfceb; }"; } else { return ""; } } }

Obviously, this is a very hard-coded approach. Configurable colors could be stored in the database and retrieved from a service here. A

CssBuilder

could be invented to do the heavy lifting, and caching could be added since the CSS won’t change much.

Create multi tenant microservice using Springboot, Hibernate and Postgres
Create multi tenant microservice using Springboot, Hibernate and Postgres

Dynamic DataSource Routing

In this section, we’ll describe the general idea behind the Database per Tenant model.

4.AbstractRoutingDataSource

The general idea to implement multi-tenancy with Spring Data JPA is routing data sources at runtime based on the current tenant identifier. In order to do that, we can use AbstractRoutingDatasource to dynamically determine the actual DataSource based on the current tenant. Let’s create a MultitenantDataSource class that extends the AbstractRoutingDataSource class:


public class MultitenantDataSource extends AbstractRoutingDataSource {
@Override
protected String determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}

The AbstractRoutingDataSource routes getConnection calls to one of the various target DataSources based on a lookup key. The lookup key is usually determined through some thread-bound transaction context. So, we create a TenantContext class for storing the current tenant in each request:


public class TenantContext {
private static final ThreadLocal

CURRENT_TENANT = new ThreadLocal<>();
public static String getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void setCurrentTenant(String tenant) {
CURRENT_TENANT.set(tenant);
}
}

We use a ThreadLocal object for keeping the tenant ID for the current request. Also, we use the set method to store the tenant ID and the get() method to retrieve it.

4.Setting Tenant ID per Request

After this configuration setup, when we perform any tenant operation, we need to know the tenant ID before creating any transaction. So, we need to set the tenant ID in a Filter or Interceptor before hitting controller endpoints. Let’s add a TenantFilter for setting the current tenant in TenantContext:


@Component
@Order(1)
class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String tenantName = req.getHeader("X-TenantID");
TenantContext.setCurrentTenant(tenantName);
try {
chain.doFilter(request, response);
} finally {
TenantContext.setCurrentTenant("");
}
}
}

In this filter, we get the tenant ID from the request header X-TenantID and set it in TenantContext. We pass control down the chain of filters. Our finally block ensures that the current tenant is reset before the next request. This avoids any risk of cross-tenant request contamination. In the next section, we’ll implement the tenants and data source declaration in the Database per Tenant model.

Conclusion

In this article, we looked at different multi-tenancy models. We described the required class for adding multi-tenancy in the Spring Boot application using Spring Data JPA for Separate Databases Shared Database and Separate Schema models. Then, we set up the required environment for testing the multi-tenancy in the PostgreSQL database. Finally, we added security to the tenants using JWT. As always, the full source code of this tutorial is available over on GitHub.

How to Build a Multitenant Application: A Hibernate Tutorial

In the realm of enterprise software, especially for software provided as a service, multitenancy ensures that data is truly isolated for each client within a shared instance of software. Among its numerous benefits, multitenancy can greatly simplify release management and cut down costs.

In this article, Toptal Freelance Software Engineer André William Prade Hildinger shows us how Hibernate, a persistence framework for Java, makes implementing a multitenant Java EE application easier than it sounds.

In the realm of enterprise software, especially for software provided as a service, multitenancy ensures that data is truly isolated for each client within a shared instance of software. Among its numerous benefits, multitenancy can greatly simplify release management and cut down costs.

In this article, Toptal Freelance Software Engineer André William Prade Hildinger shows us how Hibernate, a persistence framework for Java, makes implementing a multitenant Java EE application easier than it sounds.

Multitenant Mystery  Only Rockers in the Building by Thomas Vitale @ Spring I/O 2023
Multitenant Mystery Only Rockers in the Building by Thomas Vitale @ Spring I/O 2023

Final Words

This is not the only solution to create multitenancy applications in the Java world, but it is a simple way to achieve this.

One thing to keep in mind is that Hibernate doesn’t generate DDL when using multitenancy configuration. My suggestion is to take a look at Flyway or Liquibase, which are great libraries to control database creation. This is a nice thing to do even if you are not going to use multitenancy, as the Hibernate team advises to not use their auto database generation in production.

The source code used to create this article and environment configuration can be found at github.com/andrehil/JavaEEMT

Located in Forest Park, IL, United States

Member since February 29, 2016

Security Test

8.JWT Generation

Let’s generate the JWT for the username user. To do that, we post the credentials to /login endpoints: Let’s check the token:


eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoidGVuYW50XzEiLCJleHAiOjE2NTk2MDk1Njd9.

When we decode the token, we find out the tenant ID sets as the audience claim:


{
"sub": "user",
"aud": [
"tenant_1"
],
"iat": 1705473402,
"exp": 1705559802
}

8.Sample Request

Let’s create a post request for inserting an employee entity using the generated token: We set the generated token in the Authorization header. The tenant ID has been extracted from the token and set in the TenantContext.

Multi-tenant Architecture for SaaS
Multi-tenant Architecture for SaaS

Practical Use Of The Resolver

So, how could the resolver actually contain the right name of the schema?

One way to achieve this is to keep an identifier in the header of all requests and then create a filter to inject the name of the schema.

Let’s implement a filter class to exemplify the usage. The resolver can be accessed through Hibernate’s SessionFactory, so we will take advantage of that to get it and inject the right schema name.


@Provider public class AuthRequestFilter implements ContainerRequestFilter { @PersistenceUnit(unitName = "pu") private EntityManagerFactory entityManagerFactory; @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory(); final SchemaResolver schemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver(); final String username = containerRequestContext.getHeaderString("username"); schemaResolver.setTenantIdentifier(username); } }

Now, when any class gets an EntityManager to access the database, it will be already configured with the right schema.

For the sake of simplicity, the implementation shown here is getting the identifier directly from a string in the header, but it is a good idea to use an authentication token and store the identifier in the token. If you are interested in knowing more about this subject, I suggest taking a look at JSON Web Tokens (JWT). JWT is a nice and simple library for token manipulation.

Test

6.Creating Databases for Tenants

First, we need to define two databases in PostgreSQL: After that, we create an employee table in each database using the below script:


create table employee (id int8 generated by default as identity, name varchar(255), primary key (id));

6.Sample Controller

Let’s create an EmployeeController class for creating and saving the Employee entity in the specified tenant in the request header:


@RestController
@Transactional
public class EmployeeController {
@Autowired
private EmployeeRepository employeeRepository;
@PostMapping(path = "/employee")
public ResponseEntity

createEmployee() {
Employee newEmployee = new Employee();
newEmployee.setName("Baeldung");
employeeRepository.save(newEmployee);
return ResponseEntity.ok(newEmployee);
}
}

6.Sample Request

Let’s create a post request for inserting an employee entity in tenant ID tenant_1 using Postman: Moreover, we send a request to tenant_2: After that, when we check the database, we see that each request has been saved in the related tenant’s database.

End to end multi tenant demo & implementation
End to end multi tenant demo & implementation

Getting Started

If you are a more experienced Java developer and know how to configure everything, or if you already have your own Java EE project, you can skip this section.

First, we have to create a new Java project. I am using Eclipse and Gradle, but you can use your preferred IDE and building tools, such as IntelliJ and Maven.

If you want to use the same tools as me, you can follow these steps to create your project:

  • Install Gradle plugin on Eclipse
  • Click on File -> New -> Other…
  • Find Gradle (STS) and click Next
  • Inform a name and choose Java Quickstart for sample project
  • Click Finish

Great! This should be the initial file structure:


javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle

You can delete all files that come inside the source folders, as they are just sample files.

To run the project, I use Wildfly, and I will show how to configure it (again you can use your favorite tool here):

  • Download Wildfly: http://wildfly.org/downloads/ (I am using version 10)
  • Unzip the file
  • Install the JBoss Tools plugin on Eclipse
  • On the Servers tab, right-click any blank area and choose New -> Server
  • Choose Wildfly 10.x (9.x also works if 10 is not available, depending on your Eclipse version)
  • Click Next, choose Create New Runtime (next page) and click Next again
  • Choose the folder where you unzipped Wildfly as Home Directory
  • Click Finish

Now, let’s configure Wildfly to know the database:

  • Go to the bin folder inside your Wildfly folder
  • Execute add-user.bat or add-user.sh (depending on your OS)
  • Follow the steps to create your user as Manager
  • In Eclipse, go to the Servers tab again, right-click on the server you created and select Start
  • On your browser, access http://localhost:9990, which is the Management Interface
  • Enter the credentials of the user you just created
  • Deploy the driver jar of your database:

    1. Go to the Deployment tab and click Add
    2. Click Next, choose your driver jar file
    3. Click Next and Finish
  • Go to the Configuration tab
  • Choose Subsystems -> Datasources -> Non-XA
  • Click Add, select your database and click Next
  • Give a name to your data source and click Next
  • Select the Detect Driver tab and choose the driver you just deployed
  • Enter your database information and click Next
  • Click Test Connection if you want to make sure the information of the prior step is correct
  • Click Finish
  • Go back to Eclipse and stop the running server
  • Right-click on it, select Add and Remove
  • Add your project to the right
  • Click Finish

Alright, we have Eclipse and Wildfly configured together!

This is all the configurations required outside of the project. Let’s move on to the project configuration.

Cross-tenant security breaches

The web application works fine at this stage, but unfortunately, it’s not fully secure yet. Once a user authenticates, she’ll be handed a cookie called “JSESSIONID” that could appear as if it’s tenant-specific, but it isn’t. When a request is made using this cookie, Spring Security will check the validity of this cookie without taking tenants into account. In fact, Spring Security doesn’t know what a tenant is.

Let’s imagine I’ve authenticated against

tenant2

, where I’m a tenant administrator. I’ve been given a cookie (yum!) that I will now use to make hand-crafted requests against

tenant1

:


DELETE http://tenant1.localhost:8080/api/posts/1 Cookie: JSESSIONID=DF07D6D7C7CB9652830ABB3E108F20C7

Without any additional checks, I’ll be able to delete the posts of the other tenants. (CSRF has been left out of the equation in the example)

More filters

This

TenantAuthorizationFilter

compares the tenant of the request (taken from the URL) against the tenant of the user (taken from

CustomUserDetails

).


public class TenantAuthorizationFilter extends OncePerRequestFilter { private static final Logger LOGGER = Logger.getLogger(TenantAuthorizationFilter.class.getName()); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { var tenantId = TenantContext.getCurrentTenantId(); var authentication = SecurityContextHolder.getContext().getAuthentication(); var user = authentication == null ? null : (CustomUserDetails) authentication.getPrincipal(); var userTenantId = user == null ? null : user.getTenantId(); if (user == null || Objects.equals(tenantId, userTenantId)) { chain.doFilter(request, response); } else { LOGGER.warning("Attempted cross-tenant access."); response.setStatus(FORBIDDEN.value()); } } @Override protected boolean shouldNotFilter(HttpServletRequest request) { return request.getRequestURI().startsWith("/webjars/") || request.getRequestURI().startsWith("/css/") || request.getRequestURI().startsWith("/js/") || request.getRequestURI().endsWith(".ico"); } }

When should this filter kick in?Indeed, after authentication has happened (because

CustomUserDetails

must be created first):


@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // More config here... .addFilterBefore(new TenantFilter(tenantRepository), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(new TenantAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)

Speaking of cookies, my favorites are dinosaur coo–I mean, cookies are domain-specific inside of a browser. If you have a cookie for

tenant1.localhost

, the browser will not use it when making requests against

tenant2.localhost

. That’s great because tenants’ websites will feel separated and disconnected, as they should be.That also means that the problem highlighted above only applies to hand-crafted HTTP requests (Postman,

.http

file, …).

Schematically, the situation that we’ve got ourselves into now looks like this:

Multi tenant Database Architecture: 3 ways to build a Database Multi tenancy for a SaaS application
Multi tenant Database Architecture: 3 ways to build a Database Multi tenancy for a SaaS application

Overview

Multi-tenancy refers to an architecture in which a single instance of a software application serves multiple tenants or customers. It enables the required degree of isolation between tenants so that the data and resources used by tenants are separated from the others. In this tutorial, we’ll see how to configure multi-tenancy in a Spring Boot application with Spring Data JPA. Also, we add security to tenants using JWT.

Writing a multi-tenant web app with Java and Spring

Hello everyone.

The goal for this post is to discuss how to develop and deploy to Tomcat an example of a multi-tenant Java web application.

Before we start, here’s a couple of links that discuss the principles and challenges involved:


https://en.wikipedia.org/wiki/Multitenancy https://azure.microsoft.com/en-us/documentation/articles/dotnet-develop-multitenant-applications

Here’s the list of software you’ll need for this project:

  1. Java 1.8
  2. Spring 4
  3. Tomcat 8
  4. PostgreSQL 9.4
  5. Maven 2
  6. Eclipse IDE 4.5
  7. EclipseLink 2.6.3

First, let’s consider the major steps needed to finish the project:

  1. Install and configure the server environment where the Java app will be deployed to
  2. Choose the data separation architecture at the database level
  3. Write the application
  4. Deploy to Tomcat

Now, let’s consider the details of each of the above steps:

  1. Install and configure the server

    Because item number 1 above is time-consuming and may be new to a number of developers, I actually wrote a companion article dealing specifically with that: Setting up Tomcat + PostgreSQL for multi-tenant web apps in Linux.

  2. Choose the data segregation architecture

    Data segregation can be achieved in a number of ways for multi-tenancy apps. One of them is by creating different database “schemas”, where each tenant’s data set is stored in a different schema of the same database. That’s the approach I’ve chosen for the sample application we’ll discuss.

Using this technique, the database is organized as depicted in the following screenshot:

This allows the application to preface the table name by the tenant’s identification name as the schema. For example, for tenant1’s PRODUCT table, the application refers to it as tenant1.PRODUCT.

  1. Write the applicationI used Java + Spring + JPA + PostgreSQL to develop this application.

Please keep in mind that this application is contrived in some ways because my goal was to experiment with some concepts and get a refresher on others. For example, I wanted to find out how well – and how much coding would be required for – JPA to handle one-to-many relationships for database records; it actually require little extra code but there is definitely a learning curve to anyone new to this.

And especially I wanted to find out how much change would be required to migrate a single-tenant web app to a multi-tenant one. The design I chose is simple but works very well: One single database with multiple schemas (one per tenant) as shown in the picture above.

Here’s the screenshot of the database structure showing 2 tenants:

So, here’s a list of different techniques and technologies I employed in this app:

AJAX: nothing too fancy. In this app, I used it to invoke a web service and update only a portion of the page; who could possibly tolerate a full page refresh every time?Spring: I used multiple modules of Spring: Spring MVC, Spring Security, Java only Spring configuration through Java annotations.Pictures: I wanted to get a refresher on multi-part picture upload and handling of response in JavaScript.JPA: I chose EclipseLink, but could very easily have chosen Hibernate, which is another JPA provider.Restful web services: Spring allows you to easily create RESTful webservice. You just need to correctly configure your “controller” through Java annotations.JSON: JSON is used in most – if not all – responses from the web services.Multi-tenancy: Take a look at the RepositoryUtilityImpl class, which is a Singleton, and you’ll see that I use the tenant’s name string as schema name, also known as table qualifer.Database: I used PostgreSQL, but you could easily migrate it to another DB by making a few changes to persistence.xml and the DB.properties file where the driver name is read from.

Again, I have to stress that this is just a contrived demo web app that I wrote in my spare time; hence, much can be improved. For example, of course a USER table would have to be created; will be working on that next. However, the multi-tenancy principle is there: data is securely separate from one tenant to the next.

  1. Deploy the app to TomcatAfter this is all finished and tested locally as a single instance, the application can finally be copied to multiple directories in Tomcat and renamed to ROOT.war. There will be one directory for each of the tenants we have in our portfolio of clients.

Here’s a screenshot of what tenant1 sees on the Menu page of the application:

And here’s what tenant2 sees on the Menu page:

You haven’t even started yet and already got 2 big clients?!!! Wow!

Having a sub-domain named after its company’s name gives the client/tenant a sense of ownership; he will also be able to change some aspects of the tenant’s custom data at the Tenant Info page.

Is that sub-domain strategy and data segregation what you had in mind when you thought about multi-tenancy? Please share your comments below.

And lastly, and very important!, here’s the link for the source code and the WAR file:

https://github.com/blueriversys/SpringMvcJpaApp

Let’s create a web application to serve multiple different clients or to host a range of sub-sites and platforms.Spring Boot will be our companion on this trip with JPA/Hibernate taking care of storage and persistence.

Tenants must be disconnected from each other, and users should be able to register to and contribute to the different tenants’ websites transparently. In short, users should be oblivious to the fact that the tenants’ web applications are part of the one single Spring application.

What this article will be focusing on:

  • The web application experience. We want to ensure a maintainable application across different tenants’ URLs, and security must be kept in check. Each tenant should have their own (sub)domain.
  • We’re using a single database and shared schema. It allows us to reason about tenants on a design level and simplifies data source access. There are many articles and online sources that tackle the challenge of handling multiple data sources.

Our POC will use subdomains of localhost such as

tenant1.localhost

and

tenant2.localhost

to mimic the actual experience.When I register an account at

tenant1.localhost

, I won’t (yet) have an account at

tenant2.localhost

. In fact, as a user, I should have no clue that both tenants are even served by the same Spring application. I should be able to create an account at

tenant2

using the same email address as for

tenant1

.

The application itself will have “posts” on it. For example, you can think of them as blog posts or news posts.

  • Unauthenticated users should be able to see all posts.
  • Authenticated users should be able to contribute by adding new posts.
  • Tenant administrators should be able to both add and delete posts.

There will be a global administrator role as well. There are no plans to implement anything meaningful for the global administrator in this POC. It could be expanded by adding features to create new tenants or upgrading users to tenant administrators.

Technically, we’ll use Spring Web with MVC/Thymeleaf and a REST API. I’ve set up the project using Gradle, Spring Boot 3 (Spring 6), and PostgreSQL.

The end result of this journey can be found here.

QuickAdminPanel: Module Multi-Tenancy Demo
QuickAdminPanel: Module Multi-Tenancy Demo

Bootstrapping Project

Now that we have Eclipse and Wildfly configured and our project created, we need to configure our project.

The first thing we are going to do is to edit build.gradle. This is how it should look:


apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse' apply plugin: 'eclipse-wtp' sourceCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { jcenter() } eclipse { wtp { } } dependencies { providedCompile 'org.hibernate:hibernate-entitymanager:5.0.7.Final' providedCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.14.Final' providedCompile 'javax:javaee-api:7.0' }

The dependencies are all declared as “providedCompile”, because this command doesn’t add the dependency in the final war file. Wildfly already has these dependencies, and it would cause conflicts with the app’s ones otherwise.

At this point, you can right-click your project, select Gradle (STS) -> Refresh All to import the dependencies we just declared.

Time to create and configure the “persistence.xml” file, the file that contains the information that Hibernate needs:

  • In the src/main/resource source folder, create a folder called META-INF
  • Inside this folder, create a file named persistence.xml

The content of the file must be the something like the following, changing jta-data-source to match the datasource you created in Wildfly and the

package com.toptal.andrehil.mt.hibernate

to the one you are going to create in the next section (unless you choose the same package name):



java:/JavaEEMTDS

Summary

As you know, all source code is here.All multitenant specifics are covered above, but I recommend pulling the repository for a more complete picture. Do make sure to read the

README.md

for instructions on setting up the database. All you need is docker and a JVM.

I hope this writeup has given you the information you were looking for! Any comments are appreciated! Definitely let me know if you have any suggestions or recommendations.

Develop Multi Module (Microservices) Spring Boot Project Architecture | EnggAdda
Develop Multi Module (Microservices) Spring Boot Project Architecture | EnggAdda

A controller Example

Whenever someone creates a new post, the application needs to know two things:

  • Who is the user (author) of this new post? Easy: we use

    @AuthenticationPrincipal CustomUserDetails user

    .
  • Which tenant is this new post supposed to be a part of? Also easy: we call

    TenantContext.getCurrentTenantId()

    .

Like so:


@PostMapping("add_post") @PreAuthorize("isAuthenticated() && !hasRole('ADMINISTRATOR')") public String addPost(@AuthenticationPrincipal CustomUserDetails user, @Valid NewPostViewModel postVm) { var tenantId = TenantContext.getCurrentTenantId(); postService.addPost(user.getUserId(), tenantId, postVm.getText()); return "redirect:/posts"; }

(

@PreAuthorize

is part of Spring Method Security)

However,

TenantContext.getCurrentTenantId

is way too much typing, and I’m way too lazy so…We create a custom annotation:


@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TenantId { }

… a resolver:


public class TenantResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterAnnotation(TenantId.class) != null && parameter.getParameterType().getTypeName().equals("long"); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { return TenantContext.getCurrentTenantId(); } }

… and activate it:


@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addArgumentResolvers(List

argumentResolvers) { argumentResolvers.add(new TenantResolver()); } }

Now we can do this(!):


@PostMapping("add_post") @PreAuthorize("isAuthenticated() && !hasRole('ADMINISTRATOR')") public String addPost(@TenantId long tenantId, @AuthenticationPrincipal CustomUserDetails user, @Valid NewPostViewModel postVm) { postService.addPost(user.getUserId(), tenantId, postVm.getText()); return "redirect:/posts"; }

I’ve added support for

@Tenant String tenant

parameters as well. Very convenient because

@ControllerAdvice

can be used to add the tenant to the MVC model for all controllers.


@ControllerAdvice public class GlobalControllerAdvice { private static final Logger LOGGER = Logger.getLogger(GlobalControllerAdvice.class.getName()); @ModelAttribute("tenant") public String populateTenantName(@Tenant String tenant) { return tenant; } }

On any of my Thymeleaf pages, I can now show the tenant:

… or test on it:

Check the full source here.

Hibernate Classes

The configurations added to persistence.xml point to two custom classes MultiTenantProvider and SchemaResolver. The first class is responsible for providing connections configured with the right schema. The second class is responsible for resolving the name of the schema to be used.

Here is the implementation of the two classes:


public class MultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService { private static final long serialVersionUID = 1L; private DataSource dataSource; @Override public boolean supportsAggressiveRelease() { return false; } @Override public void injectServices(ServiceRegistryImplementor serviceRegistry) { try { final Context init = new InitialContext(); dataSource = (DataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name } catch (final NamingException e) { throw new RuntimeException(e); } } @SuppressWarnings("rawtypes") @Override public boolean isUnwrappableAs(Class clazz) { return false; } @Override public

T unwrap(Class

clazz) { return null; } @Override public Connection getAnyConnection() throws SQLException { final Connection connection = dataSource.getConnection(); return connection; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); try { connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { try { connection.createStatement().execute("SET SCHEMA 'public'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [public]", e); } connection.close(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } }


The syntax being used in the statements above work with PostgreSQL and some other databases, this must be changed in case your database has a different syntax to change the current schema.


public class SchemaResolver implements CurrentTenantIdentifierResolver { private String tenantIdentifier = "public"; @Override public String resolveCurrentTenantIdentifier() { return tenantIdentifier; } @Override public boolean validateExistingCurrentSessions() { return false; } public void setTenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; } }

At this point, it is already possible to test the application. For now, our resolver is pointing directly to a hard-coded public schema, but it is already being called. To do this, stop your server if it is running and start it again. You can try to run it in debug mode and place breakpoint at any point of the classes above to check if it is working.

ASP.NET Multi-Tenant SaaS App in 20 Minutes (EF Core) - Free Tutorial + GitHub Code Project
ASP.NET Multi-Tenant SaaS App in 20 Minutes (EF Core) – Free Tutorial + GitHub Code Project

How to Use All of This

With everything configured, there is nothing else needed to do in your entities and/or classes that interact with

EntityManager

. Anything you run from an EntityManager will be directed to the schema resolved by the created filter.

Now, all you need to do is to intercept requests on the client side and inject the identifier/token in the header to be sent to the server side.

The link at the end of the article points to the project used to write this article. It uses Flyway to create 2 schemas and contains an entity class called Car and a rest service class called

CarService

that can be used to test the project. You can follow all the steps below, but instead of creating your own project, you can clone it and use this one. Then, when running you can use a simple HTTP client (like Postman extension for Chrome) and make a GET request to http://localhost:8080/javaee-mt/rest/cars with the headers key:value:

  • username:joe; or
  • username:fred.

By doing this, the requests will return different values, which are in different schemas, one called joe and the other one called “fred”.

Spring Security

Spring Security will provide a foundation for authentication and authorization. Let’s tag a

@Configuration

class with

@EnableWebSecurity

and provide a bean of type

SecurityFilterChain

:


@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

Static assets should fall outside of our security requirements, so we’ll add matcher rules such as :


antMatcher(HttpMethod.GET, "/js/**")).permitAll()

I’m assuming a basic knowledge of Spring Security. So far, nothing special yet.

Tenant?… what tenant?

All HTTP traffic to

tenant1.localhost

,

tenant2.localhost

, and

localhost

will arrive at our Spring application (assuming port 8080). We need a way to distinguish between them and

OncePerRequestFilter

will be our tool of choice. I’ll create one and call it

TenantFilter

. This request filter will grab the tenant from the URL and store it so that we can check it later when further processing the request (i.e., in a controller).Each web request will be handled by a separate thread, so an excellent place to store this little piece of data is Java’s

ThreadLocal

. (In the future, this should even be compatible with Project Loom’s virtual threads.)

One of only a few places in a Spring application where

static

can be used meaningfully:


public class TenantContext { private static final ThreadLocal

currentTenant = new ThreadLocal<>(); private static final ThreadLocal

currentTenantId = new ThreadLocal<>(); public static String getCurrentTenant() { return currentTenant.get(); } public static void setCurrentTenant(String tenant) { currentTenant.set(tenant); } // More getters/setters }


The filter will use the context class by calling its setters:


public class TenantFilter extends OncePerRequestFilter { private final TenantRepository tenantRepository; public TenantFilter(TenantRepository tenantRepository) { this.tenantRepository = tenantRepository; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { var tenant = getTenant(request); var tenantId = tenantRepository.findBySlug(tenant).map(Tenant::getId).orElse(null); if (tenant != null && tenantId == null) { response.setStatus(NOT_FOUND.value()); // Attempted access to non-existing tenant return; } TenantContext.setCurrentTenant(tenant); TenantContext.setCurrentTenantId(tenantId); chain.doFilter(request, response); } @Override protected boolean shouldNotFilter(HttpServletRequest request) { return request.getRequestURI().startsWith("/webjars/") || request.getRequestURI().startsWith("/css/") || request.getRequestURI().startsWith("/js/") || request.getRequestURI().endsWith(".ico"); } private String getTenant(HttpServletRequest request) { var domain = request.getServerName(); var dotIndex = domain.indexOf("."); String tenant = null; if (dotIndex != -1) { tenant = domain.substring(0, dotIndex); } return tenant; } }

We must override two methods:


  • doFilterInternal

    : This method’s first -essential- responsibility is to determine if we want to continue processing this request. If the answer is yes, then we call

    chain.doFilter

    . Not calling the chain means the end of the road for this request. The other task is to get the tenant’s code (slug) from the request URL and to store it for later use. A repository is used to retrieve the ID of the tenant (handy for later!).

  • shouldNotFilter

    : tells Spring when this filter is relevant.

We only want to support tenants that exist in our database as well as

null

(=no tenant). That’s why we’ve added a 404 check.

UserDetailsService

The first place where we will read this

ThreadLocal

information is from a custom

UserDetailsService

. The method to override here is

loadUserByUsername

:


@Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { var tenant = TenantContext.getCurrentTenant(); if (tenant != null) { return loadUser(email, tenant); } else { return loadGeneralAdmin(email); } } private UserDetails loadUser(String email, String tenant) { var user = userRepository.findUser(email, tenant) .orElseThrow( () -> new UsernameNotFoundException( "'" + email + "' / '" + tenant + "' was not found.")); var auths = new ArrayList

(); auths.add(new SimpleGrantedAuthority(user.getRole().getRoleName())); return new CustomUserDetails(user.getEmail(), user.getPassword(), user.getId(), user.getTenant().getId(), auths); } private UserDetails loadGeneralAdmin(String email) { var admin = userRepository.findGeneralAdmin(email).orElseThrow( () -> new UsernameNotFoundException( "'" + email + "' was not found as a general admin.")); var auths = new ArrayList

(); auths.add(new SimpleGrantedAuthority(ADMINISTRATOR.getRoleName())); return new CustomUserDetails(admin.getEmail(), admin.getPassword(), admin.getId(), null, auths); } }


If the tenant is

null

, we know the user is accessing

localhost

without a subdomain. This part of our application will be used to host global administration pages.If the tenant is not

null

, then we must select a user filtering on both email and tenant.FYI,

userRepository.findGeneralAdmin

filters by email and tenant equal to

null

.I’ve created a

CustomUserDetails

class to extend Spring’s

User

. It contains the user ID and tenant ID as well. This will come in handy later.

Enabling the filter

We can activate the request filter from within

WebSecurityConfig

:


@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // More config here... .addFilterBefore(new TenantFilter(tenantRepository), UsernamePasswordAuthenticationFilter.class)

The before-part is essential here.

UserDetailsService

will read from

TenantContext

, so the filter must kick in before authentication is done.

Adding multi tenancy to an existing Laravel web application
Adding multi tenancy to an existing Laravel web application

Multi-Tenancy Models

There are three main approaches to multi-tenant systems:

  • Separate Database
  • Shared Database and Separate Schema
  • Shared Database and Shared Schema

2.Separate Database

In this approach, each tenant’s data is kept in a separate database instance and is isolated from other tenants. This is also known as Database per Tenant:

2.Shared Database and Separate Schema

In this approach, each tenant’s data is kept in a distinct schema on a shared database. This is sometimes called Schema per Tenant:

2.Shared Database and Shared Schema

In this approach, all tenants share a database, and every table has a column with the tenant identifier:

Andre Hildinger

André is a versatile and talented developer with 10+ years of industry experience. He is skilled at Java, Java EE, JavaScript, and more.

When we talk about cloud applications where each client has their own separate data, we need to think about how to store and manipulate this data. Even with all the great NoSQL solutions out there, sometimes we still need to use the good old relational database. The first solution that might come to mind to separate data is to add an identifier in every table, so it can be handled individually. That works, but what if a client asks for their database? It would be very cumbersome to retrieve all those records hidden among the others.

The Hibernate team came up with a solution to this problem a while ago. They provide some extension points that enable one to control from where data should be retrieved. This solution has the option to control the data via an identifier column, multiple databases, and multiple schemas. This article will cover the multiple schemas solution.

So, let’s get to work!

Multitenancy Explained
Multitenancy Explained

Security

Multi-tenancy should protect customers’ data within a shared environment. This means each tenant can only access their data. Therefore, we need to add security to our tenants. Let’s build a system where the user has to log into the application and get a JWT, which is then used to prove the right to access the tenancy.

7.Maven Dependencies

Let’s start by adding the spring-boot-starter-security dependency in the pom.xml:




org.springframework.boot


spring-boot-starter-security


Also, we need to generate and verify the JWT. To do that, we add the jjwt to our pom.xml:




io.jsonwebtoken


jjwt-api


0.12.3


7.Security Configuration

First, we need to provide the authentication capability for the tenant’s user. For simplicity, let’s use the in-memory user declaration in the SecurityConfiguration class. Starting with Spring Security 5.7.0-M2 the class WebSecurityConfigurerAdapter was deprecated and is encouraged to move towards a component-based security configuration. Let’s create a bean with UserDetails:


@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user1 = User
.withUsername("user")
.password(passwordEncoder().encode("baeldung"))
.roles("tenant_1")
.build();
UserDetails user2 = User
.withUsername("admin")
.password(passwordEncoder().encode("baeldung"))
.roles("tenant_2")
.build();
return new InMemoryUserDetailsManager(user1, user2);
}

We added two users for two tenants. Moreover, we consider the tenant as a role. According to the above code, the username user and admin have access to tenant_1 and tenant_2, respectively. Now, we create a filter for the authentication of users. Let’s add the LoginFilter class:


public class LoginFilter extends AbstractAuthenticationProcessingFilter {
public LoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException {
AccountCredentials creds = new ObjectMapper().
readValue(req.getInputStream(), AccountCredentials.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(creds.getUsername(),
creds.getPassword(), Collections.emptyList())
);
}

The LoginFilter class extends AbstractAuthenticationProcessingFilter. The AbstractAuthenticationProcessingFilter intercepts a request and attempts to perform authentication using the attemptAuthentication() method. In this method, we map the user credentials to the AccountCredentials DTO class and authenticate the user against the in-memory authentication manager:


public class AccountCredentials {
private String username;
private String password;
// getter and setter methods
}

7.JWT

Now we need to generate the JWT and add the tenant ID. To do that, we override successfulAuthentication() method. This method executes after successful authentication:


@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,
FilterChain chain, Authentication auth) throws IOException, ServletException {
Collection

authorities = auth.getAuthorities();
String tenant = "";
for (GrantedAuthority gauth : authorities) {
tenant = gauth.getAuthority();
}
AuthenticationService.addToken(res, auth.getName(), tenant.substring(5));
}

According to the above code, we get the user’s role and add it to the JWT. To do that, we create the AuthenticationService class and addToken() method:


public class AuthenticationService {
private static final long EXPIRATIONTIME = 864_000_00; // 1 day in milliseconds
private static final String SECRETKEY = "q3t6w9zCFJNcQfTjWnq3t6w9zCFJNcQfTjWnZr4u7xADGKaPd";
private static final SecretKey SIGNINGKEY = Keys.hmacShaKeyFor(SECRETKEY.getBytes(StandardCharsets.UTF_8));
private static final String PREFIX = "Bearer";
public static void addToken(HttpServletResponse res, String username, String tenant) {
String JwtToken = Jwts.builder()
.subject(username)
.audience().add(tenant).and()
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
.signWith(SIGNINGKEY)
.compact();
res.addHeader("Authorization", PREFIX + " " + JwtToken);
}
}

The addToken method generated the JWT that contains tenant ID as an audience claim and added it to the Authorization header in the response. Finally, we add the LoginFilter in SecurityConfiguration class. As we mentioned above regarding the deprecation of WebSecurityConfigurerAdapter. In this way, we will create a bean with all the configurations:


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
final AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));
http
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/login").permitAll().anyRequest().authenticated())
.sessionManagement(securityContext -> securityContext.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new LoginFilter("/login", authenticationManager), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf.disable())
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.httpBasic(Customizer.withDefaults());
return http.build();
}

Moreover, we add the AuthenticationFilter class for setting the Authentication in the SecurityContextHolder class:


public class AuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest) req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
}

7.Getting Tenant ID from JWT

Let’s modify the TenantFilter for setting the current tenant in TenantContext:


String tenant = AuthenticationService.getTenant((HttpServletRequest) req);
TenantContext.setCurrentTenant(tenant);

In this situation, we get the tenant ID from the JWT using the getTenant() method from the AuthenticationService class:


public static String getTenant(HttpServletRequest req) {
String token = req.getHeader("Authorization");
if (token == null) {
return null;
}
String tenant = Jwts.parser()
.setSigningKey(SIGNINGKEY)
.build().parseClaimsJws(token.replace(PREFIX, "").trim())
.getBody()
.getAudience()
.iterator()
.next();
return tenant;
}

Maven Dependencies

Let’s start by declaring spring-boot-starter-data-jpa dependency in a Spring Boot application in the pom.xml:




org.springframework.boot


spring-boot-starter-data-jpa


Also, we’ll be using a PostgreSQL database, so let’s also add postgresql dependency to the pom.xml file:




org.postgresql


postgresql


runtime


The Separate Database and Shared Database and Separate Schema approaches are similar in the configuration in a Spring Boot application. In this tutorial, we focus on the Separate Database approach.

AWS re:Invent 2022 - SaaS microservices deep dive: Simplifying multi-tenant development (SAS405)
AWS re:Invent 2022 – SaaS microservices deep dive: Simplifying multi-tenant development (SAS405)

Feature Customization

In an on-premise or managed service, there exists a dedicated runtime environment for each customer. Any customization required for a customer can be done in the customer code deployed on that environment. But a multi-tenant application cannot follow the same tenet since a single runtime environment, services multiple customers. Hence a multi-tenant application has be architected for feature customization. Customization for a tenant typically falls under one of the following categories:

  • Data Customization – Addition or removal of columns in the data to be stored
  • Function Customization – The functionality executed for a specific business event can vary by customers. For eg., on approval of an expense one customer may require emails to be sent, while another customer may not require want it.
  • Process Customization – The business process can vary for each customer. For eg., one customer may want shipment to integrate into a shipper’s tracking system, while another customer may want the shipment to integrate into a fleet management system.
  • Licensing Features – The product has multiple licenses which define the functionality that is enabled for the customer.

A few points to consider when architecting for customization in a multi-tenant application are:

  • Adding, modifying or removing features for one customer should not impact other customers
  • Features added for one customer may be applicable for other customers. Hence customization cannot be tied to a single customer. The architecture should allow same features to be enabled and disabled easily for other customers.
  • Licenses can be upgraded or downgraded. Features should be grouped so that they can be easily added or removed from a customer execution environment.

Feature customization can be achieved using flags and configuration parameters, but they tend to get unwieldy and can easily become un-maintainable as the customer size and customization requirements increase.

Another architecture that can be used in customization is to split business functions as discrete modules and string them together at runtime. In SMART, we use the plug and play architecture to achieve different levels of customization.

Possible improvements and additions

  • The

    @Tenant

    parameter annotation could be made compatible with a custom

    TenantDetails

    interface (similar to Spring Security’s

    UserDetails

    ). This interface could have

    getTenant

    and

    getTenantId

    methods. The

    TenantDetails

    class could be extended with a tenant name, logo (file path), etc.
  • Error handling is quite basic right now. There’s no form validation and error reporting. Redirecting after login success/failure has not been configured.
  • Thymeleaf code could be cleaned up by moving the general administration navbar and pages into separate fragments and/or include files.
  • For cleaner controller code, Method Security Meta-Annotations such as

    @AdminOnly

    and

    @TenantUserOnly

    can be created.
  • Spring Caching can be used to cache the lookup of the tenant ID (inside

    TenantFilter

    ) and to cache calls to the

    StyleController

    because they trigger a lot!

    @Cacheable

    is incredible.
Build a Multi-Tenanted, Role-Based Access Control System
Build a Multi-Tenanted, Role-Based Access Control System

Execution Environment Isolation

The requirements for Data Isolation and Feature customization implies a execution environment isolation for each tenant. For eg., if we want to achieve “Data Customization”, it means the pojo class that is used to load and save the data needs to be defined differently for different customers. This cannot be done if the same execution environment is used for different tenants, because two versions of the same class cannot be loaded into the same class loader. This also implies that the environment in which the code is deployed cannot be used as the execution environment for all tenants. A new execution environment has to be created for every tenant and the correct data source and code base has to be enabled in the environment.

In java this can be achieved using Non-delegating Class Loaders. A new class loader can be created for each tenant and the licensed code loaded into the class loader allocated for a tenant. This gives us an opportunity to

  • Deploy code without affecting other tenants in the environment
  • Enable features for a single tenant without disturbing other tenants
  • Remove functionality by just recreating the class loader for this tenant with the correct features enabled.

Putting it all together one possible architecture that we can design is as below:

Multi-tenant architecture using Java Spring and Hibernate

The term multi-tenancy refers to software architecture in which a single instance of a software application serves multiple customers.

Multiple customers share the same application, deployed on common set of software and hardware resources, with the same data-storage mechanism. One of the highest priorities for a multi-tenant architecture is creating data that is both robust and secure enough to satisfy the tenants, while also being efficient and cost-effective to administer and maintain.

The main focus of multi-tenancy architecture is to build an application that can make distinction between shared data and isolated data used by the tenants. There are three approaches to maintain multi-tenant data:

Separate Databases

Storing tenant data in separate databases is the simplest approach to data isolation.

Shared Database, Separate Schemas

Housing multiple tenants in the same database, with each tenant having its own set of tables in separate schemas.

Shared Database, Shared Schema

Using the same database and the same set of tables to host multiple tenants’ data. Each table can include records from multiple tenants stored in any order; a TenantID column associates every record with the appropriate tenant.

Multi-tenant architecture is powerful for building SaaS applications where tenant’s specific data is isolated into separate database schemas. Besides security, data isolation allows us to address some client SLA parameters regarding query performance and metrics. Simply speaking, tenant having a small data set will query that small data set only, not the whole application data that is not necessarily proportionally distributed among the tenants.

Read more about multi-tenant WEB application in Java, where database layer makes a distinction among several tenants using shared database with separate schemas.

Please reach out to me at [email protected] for any additional comments, questions or general information.

Keywords searched by users: multi tenant java web application

Multi-Tenant Applications Using Spring Boot, Jpa, Hibernate And Postgres
Multi-Tenant Applications Using Spring Boot, Jpa, Hibernate And Postgres
Multi Tenant Architecture For A Saas Application On Aws
Multi Tenant Architecture For A Saas Application On Aws
Platform Multitenant Architecture | Salesforce Architects
Platform Multitenant Architecture | Salesforce Architects
Multi-Tenant Architectures
Multi-Tenant Architectures
How To Configure An App Services Web App As A Multitenant Application -  Youtube
How To Configure An App Services Web App As A Multitenant Application – Youtube
Multi-Tenancy Implementation Using Spring Boot, Mongodb, And Redis - Dzone
Multi-Tenancy Implementation Using Spring Boot, Mongodb, And Redis – Dzone

See more here: kientrucannam.vn

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *