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.
Related snips
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
package com.example.demo.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
Messaging with Apache Kafka
class Document < ApplicationRecord
belongs_to :owner, class_name: "User"
has_many :visibilities, class_name: "DocumentVisibility", dependent: :delete_all
scope :public_documents, -> { where(is_public: true) }
Polymorphic “Visible To” Scope with Arel
package com.example.demo.controller;
import com.example.demo.dto.FileMetadata;
import com.example.demo.service.FileStorageService;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
File upload and download handling
-- Pattern 1: Shared schema with tenant_id column
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
Multi-tenancy database patterns and strategies
Share this code
Here's the card — post it anywhere.