This blog post explains how to build a robust Spring Boot Multi-Tenancy Architecture, covering both Database-per-Tenant and Schema-per-Tenant models with dynamic routing based on request context β using Hibernate MultiTenancy support.
As SaaS applications grow, serving multiple customers (tenants) in an isolated yet scalable manner becomes critical. Thatβs where multi-tenancy comes in. Whether youβre managing separate databases per tenant or isolating tenants using schemas, Spring Boot provides the flexibility and extensibility needed to implement both.

π§© What is Multi-Tenancy?
Multi-tenancy allows a single application instance to serve multiple tenants (clients), each with isolated data and resources.
π Multi-Tenancy Strategies:
| Strategy | Description | Isolation Level |
|---|---|---|
| Database-per-Tenant | Each tenant gets a separate database | High (best for compliance) |
| Schema-per-Tenant | One DB, separate schemas per tenant | Medium |
| Table-per-Tenant | Single schema, tenant ID column in all tables | Low (complex queries) |
In this post, we’ll focus on Database-per-Tenant β the most scalable and secure strategy.
π¦ Maven Dependencies
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-web
org.postgresql
postgresql
ποΈ Project Structure
com.kscodes.springboot.advanced
β
βββ config
β βββ DataSourceBasedMultiTenantConnectionProviderImpl.java
β βββ CurrentTenantIdentifierResolverImpl.java
β
βββ tenant
β βββ TenantContext.java
β βββ TenantFilter.java
β
βββ controller
β βββ ProductController.java
β
βββ entity
β βββ Product.java
β
βββ repository
βββ ProductRepository.java
π 1. Tenant Context Storage
package com.kscodes.springboot.advanced.tenant;
public class TenantContext {
private static final ThreadLocal currentTenant = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
currentTenant.set(tenantId);
}
public static String getTenantId() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
π§Ό 2. Tenant Filter (Extract from Header)
package com.kscodes.springboot.advanced.tenant;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String tenantId = ((HttpServletRequest) request).getHeader("X-Tenant-ID");
TenantContext.setTenantId(tenantId);
try {
chain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}
βοΈ 3. Multi-Tenant Config (Database Routing)
CurrentTenantIdentifierResolverImpl
package com.kscodes.springboot.advanced.config;
import com.kscodes.springboot.advanced.tenant.TenantContext;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
String tenantId = TenantContext.getTenantId();
return (tenantId != null) ? tenantId : "default";
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
DataSourceBasedMultiTenantConnectionProviderImpl
package com.kscodes.springboot.advanced.config;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.HashMap;
public class DataSourceBasedMultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
private final Map dataSources = new HashMap<>();
public DataSourceBasedMultiTenantConnectionProviderImpl(Map sources) {
this.dataSources.putAll(sources);
}
@Override
public Connection getConnection(String tenantId) throws SQLException {
return dataSources.get(tenantId).getConnection();
}
@Override
public Connection getConnection() throws SQLException {
return getConnection("default");
}
// other methods like isUnwrappableAs, supportsAggressiveRelease etc.
}
π§© 4. Enable Multi-Tenancy in Spring Config
package com.kscodes.springboot.advanced.config;
import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.*;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.sql.DataSource;
import java.util.*;
@Configuration
public class MultiTenantJpaConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSourceBasedMultiTenantConnectionProviderImpl connectionProvider,
CurrentTenantIdentifierResolverImpl tenantResolver) {
Map properties = new HashMap<>();
properties.put("hibernate.multiTenancy", "DATABASE");
properties.put("hibernate.multi_tenant_connection_provider", connectionProvider);
properties.put("hibernate.tenant_identifier_resolver", tenantResolver);
properties.put("hibernate.hbm2ddl.auto", "update");
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setPackagesToScan("com.kscodes.springboot.advanced.entity");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
em.setJpaPropertyMap(properties);
return em;
}
}
πΎ 5. Repository + Entity
Product.java
package com.kscodes.springboot.advanced.entity;
import jakarta.persistence.*;
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
}
ProductRepository.java
package com.kscodes.springboot.advanced.repository;
import com.kscodes.springboot.advanced.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository {}
π§ͺ 6. Controller for Testing
package com.kscodes.springboot.advanced.controller;
import com.kscodes.springboot.advanced.entity.Product;
import com.kscodes.springboot.advanced.repository.ProductRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductRepository repo;
public ProductController(ProductRepository repo) {
this.repo = repo;
}
@GetMapping
public List getAll() {
return repo.findAll();
}
@PostMapping
public Product create(@RequestBody Product product) {
return repo.save(product);
}
}
Test it with different X-Tenant-ID headers to route to separate databases.
π‘οΈ Best Practices for Multi-Tenancy
| Best Practice | Benefit |
|---|---|
Use ThreadLocal with care | Prevent memory leaks |
| Configure DataSource pooling per tenant | Avoid connection exhaustion |
| Use tenant resolver at request start | For proper context in async flows |
| Add fallback to default tenant | Avoid null pointers or crashes |
| Validate tenant in header/token | Prevent spoofing |
π Strategy Comparison
| Strategy | Performance | Isolation | Complexity |
|---|---|---|---|
| Database per tenant | Medium | High | Medium |
| Schema per tenant | High | Medium | Medium |
| Column per tenant | Highest | Low | High |
π Conclusion
Implementing Spring Boot Multi-Tenancy Architecture unlocks scalability, isolation, and efficiency in SaaS systems. Whether you’re managing 10 tenants or 1000, structuring your application to dynamically route database connections and resolve tenants cleanly is the cornerstone of a successful multi-tenant architecture.