java
56 lines · 7 steps
Multi-tenant row filtering with Hibernate in Spring
Enforce per-tenant data isolation automatically using a Hibernate filter toggled by a Spring interceptor on every request.
Explained by
highlit
1@Entity
2@Table(name = "invoices")
3@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = String.class))
4@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
5public class Invoice {
6
7 @Id
8 @GeneratedValue(strategy = GenerationType.UUID)
9 private UUID id;
10
11 @Column(name = "tenant_id", nullable = false, updatable = false)
12 private String tenantId;
13
14 @Column(nullable = false)
15 private BigDecimal amount;
16
17 @Enumerated(EnumType.STRING)
18 private InvoiceStatus status;
19}
20
21@Component
22@RequiredArgsConstructor
23public class TenantFilterInterceptor implements HandlerInterceptor {
24
25 private final EntityManager entityManager;
26
27 @Override
28 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
29 String tenantId = TenantContext.getTenantId();
30 if (tenantId == null) {
31 response.setStatus(HttpStatus.FORBIDDEN.value());
32 return false;
33 }
34 entityManager.unwrap(Session.class)
35 .enableFilter("tenantFilter")
36 .setParameter("tenantId", tenantId);
37 return true;
38 }
39
40 @Override
41 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
42 entityManager.unwrap(Session.class).disableFilter("tenantFilter");
43 }
44}
45
46@Configuration
47@RequiredArgsConstructor
48public class WebConfig implements WebMvcConfigurer {
49
50 private final TenantFilterInterceptor tenantFilterInterceptor;
51
52 @Override
53 public void addInterceptors(InterceptorRegistry registry) {
54 registry.addInterceptor(tenantFilterInterceptor).addPathPatterns("/api/**");
55 }
56}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Hibernate filters let you inject a WHERE clause into every query without touching individual repositories.
- 2Enabling the filter per-request and disabling it afterward keeps tenant scoping tied to the request lifecycle and avoids leaking state across pooled connections.
- 3Centralizing tenant enforcement in an interceptor removes the risk of forgetting the tenant check in any single query.
Related explainers
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
java
public final class Slugifier { private static final Pattern NON_LATIN = Pattern.compile("[^\\w-]"); private static final Pattern WHITESPACE = Pattern.compile("[\\s]+");
Building a URL slugifier in Java
regex
unicode-normalization
string-processing
Intermediate
8 steps
java
@Repository public interface OrderRepository extends JpaRepository<Order, Long> { @Query("""
Keyset pagination with Spring Data JPA
pagination
cursor
jpa
Advanced
8 steps
java
public record FixedWidthField(String name, int start, int end) { public String extract(String line) { int from = Math.min(start, line.length());
Parsing fixed-width text in Java
records
parsing
streams
Intermediate
7 steps
java
@RestController @RequestMapping("/api/quotes") public class QuoteStreamController {
Streaming market quotes with Spring WebFlux
reactive-streams
backpressure
server-sent-events
Advanced
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/multi-tenant-row-filtering-with-hibernate-in-spring-explained-java-0cc5/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.