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.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Spring’s New Multitenancy: Serving 1,000 Tenants with One Spring Boot App Without Hand‑Writing tenant_id

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  SO10003

2. 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

springJWTsaasSpring Securityhibernatedatabase-indexmultitenancy
LuTiao Programming
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.