Deep Dive into Multitenancy Using Java Spring Boot  

What is Multitenancy? 

  • Multitenancy is an architectural approach where a single instance of a software application serves multiple customers, known as tenants. 
  • Each tenant is separated logically, meaning their data and usage are kept distinct from each other. However, they share common resources like computing power, databases, and application code. This setup allows tenants to operate independently while benefiting from shared infrastructure. 
  • Multitenancy ensures data separation while maintaining efficiency and scalability. 
  • In simpler terms, a multitenant application allows multiple organizations or users to access the same underlying software while keeping their data secure and independent from each other.  
  • This is commonly used in SaaS (Software-as-a-Service) applications. 

Why Multitenancy? 

With the growing demand for cloud-based applications, multitenancy has emerged as a preferred architecture. Here’s why: 

  • Cost Efficiency: Instead of provisioning separate application instances for each customer, a single instance serves multiple tenants, reducing operational costs. 
  • Resource Optimization: Shared infrastructure leads to better utilization of computational and storage resources. 
  • Simplified Maintenance: Updates, patches, and security fixes can be applied centrally without affecting individual tenants separately. 
  • Scalability: As new tenants join, they can be onboarded easily without provisioning separate hardware or software environments. 
  • Standardized Management: Centralized control over configurations, monitoring, and security policies. 
  • Easier Tenant Management: Provides a centralized approach to tenant onboarding, monitoring, and support. 

Drawbacks of Multitenancy 

  • Security and Data Isolation: Even though tenants are logically isolated, any vulnerability in the architecture might lead to data leaks or unauthorized access. 
  • Complex Configuration Management: Each tenant may require different configurations, which can lead to complexity in managing tenant-specific settings. 
  • Performance Bottlenecks: If tenants consume resources unevenly, one tenant’s heavy usage might impact the performance of others. 
  • Customization Limitations: Since all tenants share the same application, providing extensive customization to individual tenants can be challenging. 
  • Data Migration Complexity: Moving tenants from one instance to another (e.g., upgrading storage or shifting tenants across regions) can be difficult. 

Difference Between Multitenancy and Single-Tenancy 

Feature Multitenancy Single-Tenancy 
Cost Efficiency Lower cost per tenant Higher cost due to separate deployments 
Resource Sharing Shared infrastructure Dedicated infrastructure per tenant 
Security Logical isolation Physical isolation 
Scalability Highly scalable Requires more effort to scale 
Maintenance Centralized updates Individual updates per tenant 
Customization Limited customization Full customization per tenant 

Types of Multitenancy 

Multitenancy can be implemented in different ways, primarily based on data isolation strategies. 

  • Database-per-Tenant 
  • Schema-per-Tenant 
  • Table-per-Tenant 
  • Shared Database, Shared Schema (Column-based Multitenancy) 

Database-per-Tenant 

Each tenant has its own separate database. This approach provides high security and data isolation but can be resource- intensive as the number of tenants grows. 

Example 

  • SaaS application where each customer (tenant) gets their own database. 
  • Different companies using the same HR software but storing data separately 

Advantages 

  • Full data isolation. 
  • Allows per-tenant customization.  
  • Better security. 

Disadvantages 

  • Higher infrastructure cost. 
  • Complex maintenance when scaling 

Schema-per-Tenant 

A single database contains multiple schemas, one per tenant. This approach provides a balance between isolation and resource efficiency. 

Example 

  • A multi-client CRM system where each client has a separate schema. 
  • company1_schema, company2_schema in the same database. 

Advantages 

  • Better resource utilization than database-per-tenant.  
  • Easier maintenance than separate databases. 

Disadvantages 

  • Somewhat reduced isolation. 
  • Schema changes need to be synchronized across tenants. 

Table-per-Tenant 

Each tenant (or customer) has its own separate set of tables in the database, meaning the data for each tenant is stored in different physical tables. 

Example 

  • Tenant A’s Tables: 
  • tenant_a_orders 
  • tenant_a_customers 
  • tenant_a_products 
  • Tenant B’s Tables: 
  • tenant_b_orders 
  • tenant_b_customers 
  • tenant_b_products 

Advantages 

  • Tenant A and Tenant B cannot access each other’s data directly. 
  • You can customize the tables for each tenant, e.g., different fields for tenant_a_orders vs. tenant_b_orders. 

Disadvantages 

  • As you add more tenants, you end up with many more tables, which can become difficult to manage and may lead to database performance issues. 

Shared Database with Discriminator Column 

A single database and schema are shared among all tenants. Tenant data is identified by a tenant_id column. 

Example 

  • A SaaS blogging platform where all users store data in a shared table.    
  • users, orders, products tables with a tenant_id column. 

Advantages 

  • Most cost-efficient. 
  • Easier to scale and manage.    
  • Fast tenant onboarding. 

Disadvantages 

  • Risk of data leaks if filters fail. 
  • Harder to implement per-tenant customizations. 

Multitenancy in Java Spring Boot 

Spring Boot provides a flexible way to implement multitenancy using database isolation techniques such as separate schemas, separate databases, or tenant-aware filters. 

Key Components of Spring Boot Multitenancy 

  • Tenant Identification: Identify which tenant is making the request. This can be achieved using subdomains, request headers, or user authentication data. 
  • Data Source Routing: Using AbstractRoutingDataSource to determine the correct database/schema dynamically. 
  • Hibernate Filters: Used for row-level security in shared schema strategies. 
  • Flyway/Liquibase: For schema migrations in schema-per-tenant approaches 

Spring Boot Dependencies for Multitenancy 

Implementing multitenancy in Spring Boot requires various dependencies for database routing, data source configuration, filtering, and tenant management. The dependencies vary based on the multitenancy approach used: 

Dependency Used In Purpose 
spring-boot-starter-data- jpa All Provides JPA and Hibernate support for ORM. 
mysql-connector-java All JDBC driver for MySQL (use postgresql if using PostgreSQL). 
HikariCP All Optimized connection pooling for high-performance DB connections. 
hibernate-core All ORM framework with multitenancy support. 
spring-boot-starter-web All Exposes REST APIs for tenant management. 
spring-boot-starter-aop All Enables Aspect-Oriented Programming for tenant resolution. 
spring-boot-starter- security Optional Supports tenant-based authentication. 
flyway-core Schema-per-Tenant Handles schema migration and versioning. 
hibernate-entitymanager Shared DB Enables tenant-based filtering using Hibernate Filters. 

Tenant Resolution Mechanisms in Multitenancy 

Tenant resolution is a critical aspect of multitenancy, as it determines which tenant’s data should be used for a request. There are multiple ways to resolve tenants, depending on the use case and system design. Below are the most common mechanisms: 

Mechanism Best for Pros Cons 
HTTP Headers REST APIs & Microservices Stateless, simple Requires clients to send headers 
Subdomains SaaS apps, Web applications User-friendly, secure Requires DNS configuration 
JWT Tokens OAuth-based authentication Secure, stateless Hard to revoke tokens 
Session-Based Monolithic web apps Easy to implement Not scalable for microservices 

Example of Schema based Multitenancy. 

Folder structure for schema based multitenancy. 

Step 1: Pom.xml file 

Some require dependencies. 

Step 2: Configuratioṇ.java 

Configuration.java file to setup multitenant environment. 

@Configuration 
@EnableTransactionManagement 
@EnableConfigurationProperties 
@EnableJpaRepositories( 
        basePackages = {“com.multitenancy.demo.repository”} 
) 
public class Configurations implements WebMvcConfigurer { 
 
    @Autowired 
    private MultiTenantManager multiTenantManager; 
    @Autowired 
    private TenantInterceptor tenantInterceptor; 
 
    @Override 
    public void addInterceptors(InterceptorRegistry registry) { 
 
        registry.addInterceptor(tenantInterceptor); 
    } 
 
    @Bean 
    public EntityManagerFactoryBuilder entityManagerFactoryBuilder() { 
        return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), new HashMap<>(), null); 
    } 
 
    @Bean 
    public DataSource dataSource() { 
        return multiTenantManager.dataSource(); 
    } 
 
    @Bean(name = “entityManagerFactory”) 
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, 
                                                                                @Qualifier(“dataSource”) DataSource dataSource) { 
        Map<String, Object> jpaProperties = new HashMap<>(); 
        jpaProperties.put(“spring.jpa.show-sql”, false); 
        jpaProperties.put(“spring.jpa.properties.hibernate.format_sql”, false); 
        return builder 
                .dataSource(dataSource) 
                .properties(jpaProperties) 
                .packages(“com.multitenancy.demo.entities”) 
                .build(); 
    } 
 
    @Bean(name = “transactionManager”) 
    public PlatformTransactionManager transactionManager( 
            @Qualifier(“entityManagerFactory”) EntityManagerFactory entityManagerFactory) { 
        JpaTransactionManager jpa = new JpaTransactionManager(entityManagerFactory); 
        jpa.setNestedTransactionAllowed(true); 
        jpa.setRollbackOnCommitFailure(true); 
        return jpa; 
    } 
} 

Step 3: MultiTenantManager.java 

MultiTenantManager.java to route the datasource runtime and real quick. 

@Component 
public class MultiTenantManager { 
    private final static ThreadLocal<String> currentTenant = new ThreadLocal<>(); 
    private static Map<Object, Object> tenantDataSources; 
    public DataSource dataSource() { 
        AbstractRoutingDataSource multiTenantDataSource = new AbstractRoutingDataSource() { 
            @Override 
            protected Object determineCurrentLookupKey() { 
                return currentTenant.get(); 
            } 
        }; 
        tenantDataSources = new HashMap<>(); 
        tenantDataSources.put(“a”, new DataSourceConfig(“tenant_a”).getDataSource()); 
        tenantDataSources.put(“b”, new DataSourceConfig(“tenant_b”).getDataSource()); 
        multiTenantDataSource.setTargetDataSources(tenantDataSources); 
        multiTenantDataSource.setDefaultTargetDataSource(tenantDataSources.get(“a”)); 
        multiTenantDataSource.afterPropertiesSet(); 
        return multiTenantDataSource; 
    } 
    public static void setCurrentTenant(String tenantId) {   currentTenant.set(tenantId);  } 
    public static String getCurrentTenant() {   return currentTenant.get();  } 
    public static Map<Object, Object> getAllDataSource() {   return tenantDataSources; } 
    public static void clear() {  currentTenant.remove();  } 
    public static DataSource getDataSource() { 
        return (DataSource) tenantDataSources.getOrDefault(getCurrentTenant(), null); 
    } 
} 

Step 4: TenantInterceptor.java 

TenantInterceptor.java to fetch tenant id from each upcoming request and change the datasource 

@Component 
public class TenantInterceptor implements HandlerInterceptor { 
    @Override 
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) { 
        String serverName = request.getServerName(); 
        String tenant = serverName.split(“\\.”)[0]; 
        if (StringUtils.hasText(tenant)) { 
            MultiTenantManager.setCurrentTenant(tenant); 
        } 
        return Boolean.TRUE; 
    } 
    @Override 
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) { 
        MultiTenantManager.clear(); 
    } 
} 

Step 5: TenantServiceImpl.java 

Service class to find name from different schema based on datasource route. 

@Service 
public class TenantServiceImpl implements TenantService { 
    TenantRepository tenantRepository; 
    @Autowired 
    public TenantServiceImpl(TenantRepository tenantRepository) { 
        this.tenantRepository = tenantRepository; 
    } 
    @Override 
    public String getName() { 
        return tenantRepository.findById(1).map(TenantDemo::getName).orElse(“null”); 
    } 
} 

Step 6: DemoController.java 

A rest controller file to handle api of fetch name. 

@RestController 
public class DemoController { 
 
    @Autowired 
    TenantService tenantService; 
    @RequestMapping(“/”) 
    public String myName() { 
        return tenantService.getName(); 
    } 
} 

Step 7: TenantRepository.java 

Repository file to handle database operations. 

@Repository 
public interface TenantRepository extends JpaRepository<TenantDemo, Integer> { 
} 

Step 8: TenantDemo.java 

TenantDemo is an entity, reflection of database table 

@Entity @Data @Builder @Table(name = “tenant_info”) @AllArgsConstructor @NoArgsConstructor 
public class TenantDemo { 
    @Id     @Column 
    private Integer id; 
    @Column 
    private String name; 
} 

Step 9: Database setup 

Create two schema(tenant_a, tenant_b) and create table in each schema with same name(tenant_info) in database. 

Table structure. 

Table tenant_a.tenant_info 

Table tenant_b.tenant_info 

Step 10: Test our demo 

We have two tenant with name a and b. 

Tenant a should give us Google name and tenant b should give us Yahoo 

Let test with tenant a 

SUB DOMAIN: http://a.localhost:8084/ 

Here a is tenant name and output is Google.  

Now test with tenant b 

SUB DOMAIN: http://b.localhost:8084/ 

Here b is tenant name and output is Yahoo.  

Reference(s) 

https://www.baeldung.com/multitenancy-with-spring-data-jpa
https://dzone.com/articles/dynamic-multi-tenancy-using-java-spring-boot-sprin
Author
Latest Blogs

SEND US YOUR RESUME

Apply Now