MediumVeche Tech Blogs: Multi-Tenancy in Spring Boot | by Veche Engineering Solutions | Medium
Sitemap
Veche Engineering Solutions

Veche Engineering Solutions

Veche Tech Blogs: Multi-Tenancy in Spring Boot

5 min readApr 25, 2025

--

Press enter or click to view image in full size
blog by Veche https://veche.life

“Have you ever come across a situation where you needed to build the same app for multiple clients — each with their own users, data, and slight customisations?”

Maybe you’re a developer at a startup building an internal portal for different businesses. Each customer wants their own version of the product, but spinning up a separate app for every client quickly becomes unmanageable:

  • You’re duplicating codebases.
  • Managing dozens of databases.
  • Struggling to roll out updates to all clients at once.
  • Worried about accidentally mixing up customer data.

What if there was a way to serve all your customers from a single app instance, but with data isolation and flexibilitybuilt-in?

That’s exactly what multi-tenancy solves.

“Multi-tenancy is an architecture where a single application instance serves multiple customers (tenants), while keeping their data logically separated .”

There are several ways to implement multi-tenancy in spring-boot:

Get Veche Engineering Solutions’s stories in your inbox

Join Medium for free to get updates from this writer.

Introduce the main data isolation strategies with pros and cons:

  • Database per tenant — full isolation, expensive resources. Each tenant gets their separate instance of the database
  • Schema per tenant — middle ground, better isolation, harder to manage
  • Shared schema (discriminator column) — least resource use, more logic required to enforce isolation

In this article we will discuss Shared schema (discriminator column) in details because of the following reasons

  1. The database-per-tenant approach can become expensive quickly. It’s only feasible when the benefits outweigh the costs — for example, when building an enterprise-grade application or serving a small number of high-value tenants.
  2. Schema per tenant is limited by the number of schemas that can be created in the DB that you are using.

Shared Schema or Single-table column based multi-tenancy Implementation

Shared Schema (or single-table column-based) multi-tenancy stores data for all tenants in the same tables, using a tenant identifier column to separate them. In case of spring-boot, whenever a query is made tenant-identifier is added appropriately.

Press enter or click to view image in full size
Screenshot of terminal: showing hibernate log where tenant_id is automatically added

Let’s implement it now.

  • Pre-requisites: we assume that you have a working spring-boot application that connects to a MySql database.
  • Update the application.properties file to enable single-table column based multi-tenancy by adding the following properties.
# Configures Hibernate to use discriminator-based multi-tenancy
spring.jpa.properties.hibernate.multiTenancy=DISCRIMINATOR

#Tells Hibernate how to resolve the current tenant identifier (e.g., the tenant ID)
# This should point to a class which is a custom implementation of CurrentTenantIdentifierResolver
spring.jpa.properties.hibernate.tenant_identifier_resolver=com.veche.example.TenantResolver
  • Which part of request contains the tenant_id , well its up to you. A couple of ways that I have seen are either as a JWT claim or a HTTP Request header . For the sake of simplicity let’s assume it in the header named x-tenant-id .
  • First we need to implement a way to store tenantId it in a thread-safe manner.
public class TenancyContext {

private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();

private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();

public static void setCurrentTenant(String tenantId){
TenancyContext.currentTenant.set(tenantId);
}

public static String getCurrentTenant() {
return currentTenant.get();
}

public static void clear() {
currentTenant.remove();
}

}
  • Now for every request to our service, we need to extract x-tenant-id header and store it in TenancyContext implemented in the previous step. We will use OncePerRequestFilter to achieve this.
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.logging.Logger;

@Order(1)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Logger logger = Logger.getLogger(JwtAuthenticationFilter.class.getName());


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

// if your tenancy details are in JWT claims you can just replace this logic below
// to extract JWT token -> get `tenantId` claim -> store it in TenancyContext
String tenantId = request.getHeader("x-tenant-id");
TenancyContext.setCurrentTenat(tenantId)
try {
chain.doFilter(request, response);
} catch (Exception e) {
logger.severe(
String.format("Error processing request path: %s, error: %s", requestPath, e.getMessage())
);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "{\"error\": \"Internal server error\"}");
} finally {
// very inportant to avoid cross request tenanct leaks
TenancyContext.clear();
}
}

}
  • Implement the tenant resolver class that we added to application.properties in the one of the earlier steps. The value returned by the resolveCurrentTenantIdentifier method of this class will be added to sql queries by hiberntate.
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.stereotype.Component;


@Component
public class TenantResolver implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
return IdentityAndTenancyContext.getCurrentTenant() != null ? IdentityAndTenancyContext.getCurrentTenant() : "default";
}

@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
  • Then we need to add tenancy to our JPA Entity and ensure that the sql table referenced by the entity has a tenant_id column. I have used varchar(100) but it could be any other appropriate data-type.
Press enter or click to view image in full size
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.TenantId;

@Data
@Entity(name = "test_entity")
public class TestEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;

@TenantId // this is the part that does the magic.
@Column(name = "tenant_id")
private String tenantId;

public TestEntity() {
}

public TestEntity(Long id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}

}
  • Now implement a repository for that entity
import com.veche.example.entities.TestEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TestRepository extends JpaRepository<TestEntity, Long> {
}
  • Now lets implement a service and a controller to test all of this

@Service
public class TestService {
private final TestRepository testRepository;

public TestService(TestRepository testRepository,
TestMapper testMapper) {
this.testRepository = testRepository;
}

public TestDto getTestEntity(final Long id) {
return testRepository.findById(id);
}
}
@RestController
@Validated
@RequestMapping("/test")
public class TestController {
private final TestService testService;
private final TemplateWorkflowService templateWorkflowService;

public TestController(TestService testService
, TemplateWorkflowService templateWorkflowService) {
this.templateWorkflowService = templateWorkflowService;
this.testService = testService;
}

@GetMapping("/{id}")
public String test(@PathVariable String id) {

TestEntity entity = testService.getTestEntity(Long.valueOf(id));
return "OK";
}
  • Now we can test our calling the test/{id} endpoint.
curl --location 'http://192.168.0.106:8080/test/-1' \
--header 'x-tenant-id: tenant1' \s
  • To verify that tenant_id was added automatically to sql queries check hibernate logs and you should find something like this.
Hibernate: select te1_0.`id`,te1_0.`description`,te1_0.`name`,te1_0.`tenant_id` from `test_entity` te1_0 where te1_0.`id`=? and te1_0.tenant_id = ?
<==========---> 80% EXECUTING [5m 9s]
Press enter or click to view image in full size

The End…

About Veche Engineering Solutions

Veche Engineering Solutions is a global IT consultancy specializing in embedded systems, digital signal processing (DSP), and AI integration, Machine learning, web and mobile app development.

To learn more about out services or to get in touch, visit veche.life.

--

--