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