package com.example.demo.multitenancy;
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
package com.example.demo.multitenancy;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class TenantInterceptor implements HandlerInterceptor {
private static final String TENANT_HEADER = "X-Tenant-ID";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String tenantId = request.getHeader(TENANT_HEADER);
if (tenantId == null) {
// Alternatively, extract from subdomain
String host = request.getServerName();
tenantId = extractTenantFromHost(host);
}
if (tenantId != null) {
TenantContext.setTenantId(tenantId);
} else {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
TenantContext.clear();
}
private String extractTenantFromHost(String host) {
// Extract tenant from subdomain: tenant.myapp.com
if (host.contains(".")) {
return host.split("\\.")[0];
}
return null;
}
}
package com.example.demo.model;
import jakarta.persistence.*;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
@Entity
@Table(name = "products")
@FilterDef(
name = "tenantFilter",
parameters = @ParamDef(name = "tenantId", type = String.class)
)
@Filter(
name = "tenantFilter",
condition = "tenant_id = :tenantId"
)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "tenant_id", nullable = false)
private String tenantId;
private String name;
private String description;
private Double price;
@PrePersist
@PreUpdate
public void setTenant() {
this.tenantId = TenantContext.getTenantId();
}
// Getters and setters
}
package com.example.demo.config;
import com.example.demo.multitenancy.TenantContext;
import jakarta.persistence.EntityManager;
import org.hibernate.Session;
import org.springframework.stereotype.Component;
@Component
public class TenantFilter {
private final EntityManager entityManager;
public TenantFilter(EntityManager entityManager) {
this.entityManager = entityManager;
}
public void enableFilter() {
String tenantId = TenantContext.getTenantId();
if (tenantId != null) {
Session session = entityManager.unwrap(Session.class);
session.enableFilter("tenantFilter")
.setParameter("tenantId", tenantId);
}
}
}
Multi-tenancy serves multiple customers (tenants) from single application instance. Schema-per-tenant isolates data in separate databases. Shared schema with tenant ID column partitions data within tables. Discriminator-based approach uses JPA filters. Tenant resolution uses subdomain, header, or authentication. TenantIdentifierResolver determines current tenant. Connection routing switches datasources per tenant. Spring's @TenantId or custom interceptors inject tenant context. Security ensures tenant isolation—no cross-tenant data leaks. Multi-tenancy reduces infrastructure costs while providing data isolation. Proper design balances security, performance, and maintenance. It's essential for B2B SaaS applications serving enterprise customers.