java
43 lines · 8 steps
How a tenant-resolution filter works in Spring
A servlet filter reads a tenant header, validates it against the database, and binds it to thread-local context for the request.
Explained by
highlit
1@Component
2@Order(Ordered.HIGHEST_PRECEDENCE)
3public class TenantResolutionFilter extends OncePerRequestFilter {
4
5 private static final String TENANT_HEADER = "X-Tenant-ID";
6
7 private final TenantRepository tenantRepository;
8
9 public TenantResolutionFilter(TenantRepository tenantRepository) {
10 this.tenantRepository = tenantRepository;
11 }
12
13 @Override
14 protected void doFilterInternal(HttpServletRequest request,
15 HttpServletResponse response,
16 FilterChain filterChain) throws ServletException, IOException {
17 String tenantId = request.getHeader(TENANT_HEADER);
18
19 if (!StringUtils.hasText(tenantId)) {
20 response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing " + TENANT_HEADER + " header");
21 return;
22 }
23
24 Tenant tenant = tenantRepository.findByExternalId(tenantId).orElse(null);
25 if (tenant == null || !tenant.isActive()) {
26 response.sendError(HttpStatus.FORBIDDEN.value(), "Unknown or inactive tenant");
27 return;
28 }
29
30 try {
31 TenantContext.setCurrentTenant(tenant);
32 response.setHeader(TENANT_HEADER, tenant.getExternalId());
33 filterChain.doFilter(request, response);
34 } finally {
35 TenantContext.clear();
36 }
37 }
38
39 @Override
40 protected boolean shouldNotFilter(HttpServletRequest request) {
41 return request.getRequestURI().startsWith("/actuator");
42 }
43}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Running a filter at highest precedence lets you establish request-scoped context before any other component executes.
- 2Thread-local context must always be cleared in a finally block to avoid leaking state across pooled request threads.
- 3Validating identity early and failing with the right HTTP status keeps invalid requests out of your business logic.
Related explainers
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 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/how-a-tenant-resolution-filter-works-in-spring-explained-java-53b5/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.