Spring Boot
Spring Data
Multi Tenancy
Java
Microservices

Spring Boot Spring Data with multi tenancy

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Multi-tenancy in Spring Boot and Spring Data is not one feature you switch on. It is an architectural choice about how tenant data is isolated and how the application selects the right database, schema, or row subset for each request.

The first decision is the tenancy model: database-per-tenant, schema-per-tenant, or shared tables with a tenant discriminator. That decision affects everything else, including security, migrations, and repository behavior.

Choose the Multi-Tenancy Model First

The three common models are:

  • database per tenant
  • schema per tenant
  • shared tables with a tenant column

Database per tenant gives the strongest isolation but more operational overhead. Shared tables are simpler to operate but require careful filtering so one tenant never sees another tenant's data.

Spring Data repositories sit on top of that choice. They do not solve the isolation problem by themselves.

A Simple Tenant Context

Most Spring-based multi-tenant implementations start by storing the current tenant in request-scoped or thread-local context.

java
1public final class TenantContext {
2    private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();
3
4    public static void setTenant(String tenantId) {
5        CURRENT.set(tenantId);
6    }
7
8    public static String getTenant() {
9        return CURRENT.get();
10    }
11
12    public static void clear() {
13        CURRENT.remove();
14    }
15}

You would usually populate this from a request header, subdomain, token claim, or authenticated principal.

Database-Per-Tenant Routing

For database-per-tenant, a common pattern is an AbstractRoutingDataSource that selects a data source based on the current tenant.

java
1import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
2
3public class TenantRoutingDataSource extends AbstractRoutingDataSource {
4    @Override
5    protected Object determineCurrentLookupKey() {
6        return TenantContext.getTenant();
7    }
8}

Once the tenant ID is set in TenantContext, Spring routes repository access to the appropriate underlying data source.

This is one of the cleanest models for strong isolation because repositories can stay mostly normal while connection routing changes underneath.

Capturing the Tenant from the Request

A servlet filter is a simple place to set the tenant for each request.

java
1import jakarta.servlet.FilterChain;
2import jakarta.servlet.ServletException;
3import jakarta.servlet.http.HttpFilter;
4import jakarta.servlet.http.HttpServletRequest;
5import jakarta.servlet.http.HttpServletResponse;
6import java.io.IOException;
7
8public class TenantFilter extends HttpFilter {
9    @Override
10    protected void doFilter(HttpServletRequest request,
11                            HttpServletResponse response,
12                            FilterChain chain) throws IOException, ServletException {
13        try {
14            TenantContext.setTenant(request.getHeader("X-Tenant-Id"));
15            chain.doFilter(request, response);
16        } finally {
17            TenantContext.clear();
18        }
19    }
20}

That finally block matters. Forgetting to clear tenant context is one of the easiest ways to leak tenant state between requests in thread-pooled servers.

Shared-Table Multi-Tenancy Is Different

If all tenants share the same tables, data isolation depends on every query filtering by tenant ID. That can be done with Hibernate filters, specifications, or explicit query conditions.

This model is operationally cheaper but easier to get wrong. One missing predicate can turn into a cross-tenant data leak.

So the "simpler database design" often shifts complexity into query safety and testing.

Spring Data Repositories Still Need Tenant Awareness

Even if the repository interface looks ordinary, the tenant boundary must exist somewhere:

  • in routed data sources
  • in schema switching
  • in ORM filters
  • in explicit query predicates

Repositories alone do not make the application multi-tenant. They simply participate in whatever isolation strategy the persistence layer enforces.

Common Pitfalls

  • Starting with Spring Data code before deciding the actual tenancy model.
  • Using a thread-local tenant context and forgetting to clear it after the request.
  • Assuming repository interfaces automatically enforce tenant isolation.
  • Choosing shared-table multi-tenancy without strong safeguards against missing tenant filters.
  • Underestimating operational tasks such as migrations, onboarding, and per-tenant observability.

Summary

  • Multi-tenancy in Spring Boot and Spring Data starts with an architecture choice, not a repository annotation.
  • The main models are database per tenant, schema per tenant, and shared tables with a discriminator.
  • A tenant context plus routed data source is a common solution for database-per-tenant setups.
  • Shared-table designs can work, but they require disciplined tenant filtering in every data path.
  • Spring Data participates in the design, but it does not replace the need for a real tenant-isolation strategy.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.