java 119 lines · 4 tabs

Enforce Multi-Tenant Row Isolation With Hibernate @Filter Enabled Per Request in Spring Boot

Shared by codesnips Aug 2026
4 tabs
@MappedSuperclass
@FilterDef(
    name = "tenantFilter",
    parameters = @ParamDef(name = "tenantId", type = String.class)
)
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public abstract class TenantAware {

    @Column(name = "tenant_id", nullable = false, updatable = false)
    private String tenantId;

    @PrePersist
    void assignTenant() {
        if (this.tenantId == null) {
            this.tenantId = TenantContext.require();
        }
    }

    public String getTenantId() {
        return tenantId;
    }
}

@Entity
@Table(name = "invoices")
public class Invoice extends TenantAware {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(nullable = false)
    private BigDecimal amount;

    @Column(nullable = false)
    private String status;
}
4 files · java Explain with highlit

This snippet shows a discriminator-based multi-tenancy scheme where every tenant-owned table carries a tenant_id column and Hibernate transparently appends a tenant_id = :tenantId predicate to each query. The advantage over manually threading a tenant argument into every repository method is that the constraint lives in one place and is impossible to forget, which turns a whole class of cross-tenant data leaks into a non-issue.

In TenantAware entity, the mapped superclass declares a Hibernate @FilterDef named tenantFilter with a tenantId parameter and applies it with @Filter. Every concrete entity that extends TenantAware inherits the tenant_id mapping and the filter condition. The column is marked updatable = false so a row can never be reassigned to a different tenant after insert.

A defined filter is inert until it is explicitly enabled on the current Hibernate Session, and it must be re-enabled for every session because filters do not persist across transactions. That is the job of TenantFilterAspect: an @Around advice that wraps every @Transactional service method, unwraps the JPA EntityManager to a Hibernate Session, reads the current tenant from TenantContext, and calls enableFilter(...).setParameter(...). Running inside the transaction guarantees the session already exists, so the filter attaches to the very session that will execute the queries.

TenantContext holds the tenant id in a ThreadLocal, which fits Spring's thread-per-request model. TenantContextFilter is a servlet OncePerRequestFilter that extracts the tenant from a validated header (in practice this comes from the authenticated principal, not raw client input) and, crucially, clears the ThreadLocal in a finally block so pooled request threads never leak a stale tenant into the next request.

The main trade-off is that the filter only guards reads: inserts still need tenant_id populated correctly, and native queries or criteria that bypass the session filter are not covered, so a @PrePersist hook or an explicit set remains necessary. There is also an ordering pitfall — the aspect must run inside an active transaction, so the transaction advice has to be established first. This pattern suits SaaS applications with a shared schema and moderate tenant counts where a full schema-per-tenant setup would be operationally heavy.


Related snips

Share this code

Here's the card — post it anywhere.

Enforce Multi-Tenant Row Isolation With Hibernate @Filter Enabled Per Request in Spring Boot — share card
Link copied