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)