Spring’s New Multitenancy: Serving 1,000 Tenants with One Spring Boot App Without Hand‑Writing tenant_id
The article explains how Spring’s latest multitenancy support, combined with Spring Security, JWT and Hibernate’s @TenantId, lets a single Spring Boot application safely serve thousands of SaaS tenants by automatically isolating data, avoiding manual tenant_id filters, and handling indexes, native SQL, and async tasks.
1. Multitenancy Is Not “One Database Per Tenant”
Hibernate defines three multitenancy strategies: separate databases, separate schemas, or shared tables with a tenant_id column. Separate databases give the strongest isolation but become costly at scale (e.g., 1,000 tenants require 1,000 connection pools, migrations, backups). Separate schemas reduce cost but still increase operational complexity as the number of schemas grows.
The author adopts the third approach—one shared database and shared tables, each row tagged with tenant_id. This model fits SaaS projects where many tenants have modest data volumes.
orders (id, tenant_id, order_no, amount, status)Example rows:
1 shop_001 SO10001
2 shop_002 SO10002
3 shop_001 SO100032. Never Trust tenant_id From the Front‑End
Relying on a client‑provided X‑Tenant‑Id header is dangerous because an attacker can change it and access another tenant’s data. The safe source is an authenticated token (e.g., JWT) verified by Spring Security.
{
"sub": "10086",
"tenant_id": "shop_001",
"roles": ["ADMIN"]
}The backend extracts tenant_id from the security context, not from request parameters.
3. CurrentTenantIdentifierResolver Supplies the Current Tenant
Hibernate’s CurrentTenantIdentifierResolver determines the tenant for each Session. An implementation typically reads the JWT from SecurityContextHolder and returns the tenant_id claim.
@Component
public class SecurityTenantResolver implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
throw new IllegalStateException("Tenant not authenticated");
}
Jwt jwt = (Jwt) auth.getPrincipal();
String tenantId = jwt.getClaimAsString("tenant_id");
if (tenantId == null || tenantId.isBlank()) {
throw new IllegalStateException("Tenant id missing");
}
return tenantId;
}
@Override
public boolean validateExistingCurrentSessions() { return true; }
}4. Adding @TenantId to Entities
Instead of manually storing tenantId in every entity, annotate the field with @TenantId. Hibernate automatically populates it on persist and adds the tenant filter to all queries.
@Entity
@Table(name = "orders")
public class Order {
@Id
private Long id;
@TenantId
@Column(name = "tenant_id", nullable = false, updatable = false)
private String tenantId;
private String orderNo;
private BigDecimal amount;
}Calling orderRepository.findAll() now returns only rows belonging to the current tenant, equivalent to adding WHERE tenant_id = 'shop_001' behind the scenes.
5. Native SQL Does Not Get Automatic Tenant Filtering
Hibernate explicitly warns that native queries are not rewritten to include tenant_id. Developers must add the filter manually, e.g.:
@Query(value = "SELECT * FROM orders WHERE status = 'UNPAID' AND tenant_id = :tenantId", nativeQuery = true)
List<Order> findUnpaidOrders(@Param("tenantId") String tenantId);Team conventions should require every native query to contain the tenant predicate.
6. Index Design Must Include tenant_id
Unique indexes that ignore tenant_id can cause collisions across tenants. Redefine them as composite indexes, e.g.:
CREATE UNIQUE INDEX uk_tenant_order ON orders(tenant_id, order_no);
CREATE INDEX idx_tenant_status ON orders(tenant_id, status);7. Async Tasks and MQ Need Explicit Tenant Propagation
When execution leaves the HTTP request thread, the SecurityContext is lost. As a result, the tenant cannot be resolved automatically. All asynchronous jobs, message consumers, and batch processes must receive the tenant identifier explicitly (e.g., as a field in the message payload) and restore the tenant context before opening a Hibernate Session.
{
"eventId": "EV10001",
"tenantId": "shop_001",
"orderId": 10086
}Consumers then reconstruct the tenant context and proceed with database operations.
8. Final Checklist
Never trust client‑provided tenant_id; always derive it from an authenticated token.
Use Hibernate’s @TenantId and CurrentTenantIdentifierResolver for automatic query filtering.
Manually add tenant_id to every native SQL statement.
Design composite indexes that include tenant_id.
Propagate tenantId explicitly in async tasks, MQ messages, and batch jobs.
When these rules are followed, a single Spring Boot codebase can safely serve hundreds or thousands of SaaS tenants without the overhead of per‑tenant databases or schemas.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
