@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;
}
public final class TenantContext {
private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();
private TenantContext() {
}
public static void set(String tenantId) {
CURRENT.set(tenantId);
}
public static String get() {
return CURRENT.get();
}
public static String require() {
String tenantId = CURRENT.get();
if (tenantId == null) {
throw new IllegalStateException("No tenant bound to current request");
}
return tenantId;
}
public static void clear() {
CURRENT.remove();
}
}
@Aspect
@Component
public class TenantFilterAspect {
@PersistenceContext
private EntityManager entityManager;
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object enableTenantFilter(ProceedingJoinPoint pjp) throws Throwable {
String tenantId = TenantContext.get();
if (tenantId == null) {
return pjp.proceed();
}
Session session = entityManager.unwrap(Session.class);
Filter filter = session.getEnabledFilter("tenantFilter");
if (filter == null) {
filter = session.enableFilter("tenantFilter");
}
filter.setParameter("tenantId", tenantId);
filter.validate();
return pjp.proceed();
}
}
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TenantContextFilter extends OncePerRequestFilter {
private static final String TENANT_HEADER = "X-Tenant-Id";
private final TenantRegistry tenantRegistry;
public TenantContextFilter(TenantRegistry tenantRegistry) {
this.tenantRegistry = tenantRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String tenantId = request.getHeader(TENANT_HEADER);
if (tenantId == null || !tenantRegistry.isActive(tenantId)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unknown or inactive tenant");
return;
}
try {
TenantContext.set(tenantId);
chain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
Share this code
Here's the card — post it anywhere.