Multitenancy

This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production.

Three older reference books were consulted as bibliography only while preparing these pages and are not the primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

This section’s bibliography lists the reference material consulted while preparing these pages.

A multitenant application serves multiple isolated customers ("tenants") from one deployment. Hibernate supports three isolation strategies, each trading isolation strength against operational complexity.

Three approaches

Strategy Behavior

Separate database

Each tenant gets a fully separate database (its own connection, credentials, and physical storage). Strongest isolation — one tenant’s data is not even reachable from another tenant’s connection — at the highest operational cost (schema migrations, backups, and connection-pool management multiply per tenant).

Separate schema

One database, one schema per tenant, selected by switching the JDBC connection’s schema/search-path per request. Strong logical isolation with shared infrastructure (one database instance, one connection pool to size); a compromise between the other two.

Discriminator column

One database, one schema, one set of tables shared by every tenant, with a tenant_id column on every multitenant-aware row and an implicit WHERE tenant_id = ? applied to every query. Cheapest operationally (one schema to migrate, one connection pool), weakest isolation (a bug that omits the tenant filter leaks data across tenants at the query level, not the infrastructure level).

CurrentTenantIdentifierResolver and MultiTenantConnectionProvider

Two SPIs Hibernate needs regardless of strategy:

public class RequestTenantResolver implements CurrentTenantIdentifierResolver<String> {
    @Override
    public String resolveCurrentTenantIdentifier() {
        return TenantContext.getCurrentTenant(); // e.g. from a request header or JWT claim
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

CurrentTenantIdentifierResolver answers "which tenant is this unit of work for" — consulted whenever Hibernate needs a tenant identifier and none was supplied explicitly. MultiTenantConnectionProvider answers "given a tenant identifier, how do I get the right JDBC connection" — for the separate-database strategy, it routes to a different DataSource per tenant; for separate-schema, it switches schema/search-path on a shared connection pool’s connection before handing it out. The discriminator-column strategy needs neither of the connection-routing pieces — both strategies still need CurrentTenantIdentifierResolver to know which tenant’s filter to apply.

@TenantId

For the discriminator-column strategy, @TenantId marks the entity field Hibernate should automatically filter every query by, using the value CurrentTenantIdentifierResolver supplies — no manual WHERE tenant_id = :t needed in application-written HQL/Criteria queries:

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @TenantId
    private String tenantId;

    // every query against Order is automatically scoped to the current tenant
}

@TenantId is conceptually similar to a @Filter, except it is always active for the resolved tenant (not manually enabled per session) and is specifically designed around the tenant-resolution SPI above rather than an arbitrary session parameter.

Second-level-cache considerations

A shared second-level cache (see Second-Level Cache) must be tenant-aware for the separate-database and separate-schema strategies — the same entity class/id pair means a different row per tenant, so cache keys need the tenant identifier folded in; Hibernate does this automatically when multitenancy is configured. For the discriminator-column strategy, caching is naturally per-row already (the tenant_id is part of the row’s own data), but a cache region shared across tenants means one very active tenant’s cache churn can evict entries a quieter tenant would otherwise have kept hot — worth monitoring per Performance & Statistics's cache hit-rate statistics.

Spring Boot wiring note

Spring Boot has no built-in multitenancy auto-configuration; wiring CurrentTenantIdentifierResolver and (for separate-database/schema) MultiTenantConnectionProvider as Spring @Bean`s, and resolving the current tenant from a request-scoped context (a servlet filter populating a `ThreadLocal, or a header/JWT claim read per request), is application code. hibernate.multi_tenancy / hibernate.tenant_identifier_resolver/hibernate.multi_tenant_connection_provider are set via spring.jpa.properties.hibernate.*.

The three strategies compared

Separate database