Spring Boot Multi-Tenancy Architecture: A Complete Implementation Guide

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:

StrategyDescriptionIsolation Level
Database-per-TenantEach tenant gets a separate databaseHigh (best for compliance)
Schema-per-TenantOne DB, separate schemas per tenantMedium
Table-per-TenantSingle schema, tenant ID column in all tablesLow (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 PracticeBenefit
Use ThreadLocal with carePrevent memory leaks
Configure DataSource pooling per tenantAvoid connection exhaustion
Use tenant resolver at request startFor proper context in async flows
Add fallback to default tenantAvoid null pointers or crashes
Validate tenant in header/tokenPrevent spoofing

πŸ“Š Strategy Comparison

StrategyPerformanceIsolationComplexity
Database per tenantMediumHighMedium
Schema per tenantHighMediumMedium
Column per tenantHighestLowHigh

πŸ”š 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.

πŸ”— External References