Home Java Multi-Tenancy in Spring Boot: Database, Schema, and Discriminator Strategies
Advanced 6 min · July 14, 2026

Multi-Tenancy in Spring Boot: Database, Schema, and Discriminator Strategies

Learn three multi-tenancy strategies in Spring Boot 3.2 — database-per-tenant, schema-per-tenant, and discriminator column.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+ installed on your machine
  • Spring Boot 3.2 project with spring-boot-starter-data-jpa
  • MySQL 8.0 or PostgreSQL 15 running locally
  • Basic understanding of JPA and Hibernate
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

• Multi-tenancy means a single app serves multiple clients (tenants) with isolated data.\n• Three main strategies: database-per-tenant, schema-per-tenant, and discriminator column.\n• Database-per-tenant offers strongest isolation but is hardest to scale.\n• Schema-per-tenant balances isolation and operational cost.\n• Discriminator column is simplest but least secure — use only for low-compliance scenarios.

✦ Definition~90s read
What is Multi-Tenancy in Spring Boot?

Multi-tenancy in Spring Boot means configuring your data access layer so that each tenant's queries automatically route to their isolated storage — a separate database, schema, or filtered table — without leaking data between tenants.

Think of a bank with safety deposit boxes.
Plain-English First

Think of a bank with safety deposit boxes. Database-per-tenant is like each tenant renting their own vault room — total privacy but expensive. Schema-per-tenant is like each tenant having their own locked drawer within the same vault — good privacy, cheaper. Discriminator column is like all tenants sharing one big drawer with their names on envelopes — cheap but anyone could accidentally peek.

Multi-tenancy is the architectural pattern where a single instance of your application serves multiple customers — tenants — while keeping their data completely isolated. If you're building a SaaS platform for payment processing, HR management, or analytics dashboards, you'll face this problem sooner than later.

In the Spring Boot ecosystem, you have three battle-tested strategies: database-per-tenant, schema-per-tenant, and discriminator column (sometimes called shared table). Each comes with trade-offs in isolation, performance, and operational complexity. I've seen teams burn months on the wrong choice — picking discriminator for a fintech app (auditors will cry) or database-per-tenant for a 500-tenant system without connection pooling (hello, OOM).

This tutorial covers Spring Boot 3.2 with Hibernate 6.4, using realistic examples from a subscription billing domain. We'll walk through each strategy with working code, then dive into the production incidents that taught me how fragile these setups can be. By the end, you'll know exactly which strategy fits your use case and how to avoid the landmines I've stepped on.

Understanding Multi-Tenancy: The Three Strategies

Before we write code, you need to understand what each strategy actually does under the hood. I've seen developers pick database-per-tenant because 'it's the most secure' without realizing they just tripled their operational costs.

Database-per-tenant: Each tenant gets their own database. This gives you the strongest isolation — one tenant's corrupted data can't touch another's. But you also get connection pool explosion, backup nightmares (200 databases to back up), and schema migration hell. Use this for fintech, healthcare, or any scenario where compliance demands physical separation.

Schema-per-tenant: Single database, but each tenant has their own schema (namespace). In PostgreSQL, that's a schema; in MySQL, you simulate it with separate databases but same server. Isolation is strong — tenants share the same connection pool, so no pool explosion. Schema migrations run once per tenant, which is manageable with Flyway or Liquibase. This is my go-to for most B2B SaaS apps.

Discriminator column: All tenants share the same tables. A tenant_id column in every row separates data. This is the cheapest and fastest to implement, but you're one miswritten query away from a data leak. Use this only for internal tools or low-compliance scenarios where tenants trust each other.

The rest of this article builds a subscription billing system where each tenant has customers, invoices, and payment methods. We'll implement all three strategies so you can compare them side by side.

TenantAwareEntity.javaJAVA
1
@MappedSuperclass\npublic abstract class TenantAwareEntity {\n\n    @Column(name = "tenant_id", nullable = false)\n    private String tenantId;\n\n    @PrePersist\n    public void prePersist() {\n        if (tenantId == null) {\n            tenantId = TenantContext.getCurrentTenantId();\n        }\n    }\n\n    // getters and setters\n}
Output
No direct output. This is a base class for all entities.
⚠ Don't Forget @PrePersist
📊 Production Insight
In production, always validate that tenant_id is indexed in discriminator strategy. I once saw a query that scanned 10 million rows because the index was missing. The DBA was not happy.
🎯 Key Takeaway
Choose your strategy based on compliance requirements and operational budget, not just developer convenience.

What the Official Docs Won't Tell You

Spring Boot's official documentation shows you a clean, academic example of multi-tenancy. What they don't show you is the production nightmare when your TenantContext filter silently swallows an NPE and every request defaults to tenant 'null'.

Here's the cold truth: ThreadLocal is your enemy and your savior. The official docs show you how to set up a TenantContext with ThreadLocal, but they don't tell you that thread pools (used by Tomcat, WebFlux, and @Async) will leak tenant IDs between requests if you don't clean up. I've debugged a production incident where User A's invoice showed up in User B's dashboard because the thread pool reused a thread that still had User A's tenant ID in its ThreadLocal.

The fix is brutal but necessary: use a OncePerRequestFilter that sets the tenant ID before the request and guarantees cleanup in a finally block. Not in a @PreDestroy or @PostConstruct — those run at bean lifecycle, not request lifecycle. Also, never use InheritableThreadLocal unless you want your async tasks to inherit the wrong tenant.

Another thing the docs gloss over: connection pool sizing. For database-per-tenant, HikariCP's default of 10 connections per datasource will kill your database if you have 100+ tenants. You need a shared pool with a routing datasource. I'll show you that in the next section.

TenantContextFilter.javaJAVA
1
@Component\npublic class TenantContextFilter extends OncePerRequestFilter {\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request,\n                                    HttpServletResponse response,\n                                    FilterChain chain)\n            throws ServletException, IOException {\n        try {\n            String tenantId = request.getHeader("X-Tenant-Id");\n            if (tenantId == null || tenantId.isBlank()) {\n                throw new TenantNotFoundException("Missing X-Tenant-Id header");\n            }\n            TenantContext.setCurrentTenantId(tenantId);\n            chain.doFilter(request, response);\n        } finally {\n            TenantContext.clear();\n        }\n    }\n}
Output
Every request sets tenant ID in ThreadLocal. After request completes, ThreadLocal is cleared.
💡ThreadLocal Leak = Data Leak
📊 Production Insight
Add a scheduled task that logs ThreadLocal values every minute in staging. You'll catch leaks before they hit production. I've caught three this way.
🎯 Key Takeaway
ThreadLocal cleanup is non-negotiable. Use OncePerRequestFilter with a finally block. Never rely on framework magic.

Strategy 1: Database-Per-Tenant with Dynamic DataSources

This strategy creates a separate database for each tenant. The application holds a map of tenant IDs to DataSource objects. When a request comes in, we look up the tenant's datasource and use it for the entire Hibernate session.

When to use: PCI DSS, HIPAA, SOC 2 Type II — any compliance that requires physical data separation. Also useful when tenants have vastly different data volumes (one tenant has 10 rows, another has 10 billion rows).

How it works: We implement Hibernate's MultiTenantConnectionProvider and CurrentTenantIdentifierResolver. The connection provider returns a connection from the tenant-specific datasource. The identifier resolver tells Hibernate which tenant is active.

The catch: You need a way to provision new databases when a tenant signs up. I use Flyway with a per-tenant migration script that runs in a separate transaction. Also, never create datasources lazily in the request thread — you'll block the request for seconds while HikariCP initializes. Pre-warm datasources in a background thread when the tenant is created.

Here's the code for a dynamic datasource registry that creates datasources on tenant creation, not on first request.

TenantDataSourceProvider.javaJAVA
1
@Component\npublic class TenantDataSourceProvider implements MultiTenantConnectionProvider {\n\n    private final Map<String, DataSource> dataSources = new ConcurrentHashMap<>();\n    private final DataSource masterDataSource;\n\n    public TenantDataSourceProvider(DataSource masterDataSource) {\n        this.masterDataSource = masterDataSource;\n    }\n\n    public void addTenant(String tenantId, String url, String user, String pass) {\n        HikariConfig config = new HikariConfig();\n        config.setJdbcUrl(url);\n        config.setUsername(user);\n        config.setPassword(pass);\n        config.setMaximumPoolSize(5); // per tenant cap\n        dataSources.put(tenantId, new HikariDataSource(config));\n    }\n\n    @Override\n    public Connection getConnection(String tenantId) throws SQLException {\n        DataSource ds = dataSources.get(tenantId);\n        if (ds == null) {\n            throw new TenantNotFoundException("No datasource for tenant: " + tenantId);\n        }\n        return ds.getConnection();\n    }\n\n    @Override\n    public Connection getAnyConnection() throws SQLException {\n        return masterDataSource.getConnection();\n    }\n}
Output
Returns a connection from the tenant's dedicated HikariCP pool.
⚠ Connection Pool Math
📊 Production Insight
Use a health check endpoint that pings each tenant's datasource. I've seen a tenant's database go down and the app still try to route requests to it, causing 30-second timeouts.
🎯 Key Takeaway
Database-per-tenant gives strongest isolation but requires careful connection pool management. Pre-warm datasources to avoid latency spikes.

Strategy 2: Schema-Per-Tenant with Single DataSource

This is the sweet spot for most B2B SaaS applications. You have one database, one connection pool, but each tenant has their own schema (namespace). In PostgreSQL, use CREATE SCHEMA IF NOT EXISTS tenant_123. In MySQL, you simulate this with separate databases on the same server, but the principle is the same.

Advantages: Single connection pool means no pool explosion. Schema migrations can be batched. Backups are simpler — one database dump covers all tenants.

How it works: Hibernate's MultiTenantConnectionProvider returns the same connection, but then uses Connection.setSchema() (PostgreSQL) or USE database_tenant_123 (MySQL) to switch the schema. The CurrentTenantIdentifierResolver still picks the tenant ID from the request.

The gotcha: Connection.setSchema() is supported in JDBC 4.1+ but not all drivers implement it correctly. MySQL's Connector/J ignores setSchema() — you have to execute USE database_tenant_123 as a raw SQL statement. Also, schema creation must be done outside the Hibernate session, because Hibernate's DDL auto-generation doesn't handle per-schema creation well.

I use Flyway with a custom callback that runs migrations for each schema. Here's the connection provider that uses schema switching.

SchemaPerTenantConnectionProvider.javaJAVA
1
@Component\npublic class SchemaPerTenantConnectionProvider implements MultiTenantConnectionProvider {\n\n    private final DataSource dataSource;\n\n    public SchemaPerTenantConnectionProvider(DataSource dataSource) {\n        this.dataSource = dataSource;\n    }\n\n    @Override\n    public Connection getConnection(String tenantId) throws SQLException {\n        Connection connection = dataSource.getConnection();\n        // PostgreSQL: setSchema, MySQL: execute USE\n        if (isPostgres(connection)) {\n            connection.setSchema("tenant_" + tenantId);\n        } else {\n            try (Statement stmt = connection.createStatement()) {\n                stmt.execute("USE tenant_" + tenantId);\n            }\n        }\n        return connection;\n    }\n\n    @Override\n    public Connection getAnyConnection() throws SQLException {\n        return dataSource.getConnection();\n    }\n}
Output
Returns a connection with the schema set to the tenant's namespace.
🔥MySQL Workaround
📊 Production Insight
Always set a default schema for the connection pool itself. If a query runs without a tenant context, it should fail fast, not silently query the public schema.
🎯 Key Takeaway
Schema-per-tenant offers the best balance of isolation, performance, and operational simplicity for most SaaS apps.

Strategy 3: Discriminator Column (Shared Table)

This is the simplest strategy: all tenants share the same tables, and every row has a tenant_id column. You add a WHERE tenant_id = ? clause to every query. In Hibernate, you use a @Where annotation or a Hibernate filter to automatically append the condition.

When to use: Internal tools, multi-tenant admin panels, or scenarios where tenants are fully trusted (e.g., different departments in the same company). Never use this for external-facing SaaS where tenants are competitors.

The risk: One missing WHERE clause and Tenant A sees Tenant B's data. I've seen this happen in production because a developer used a native query and forgot to add the tenant filter. The fix is to enforce tenant filtering at the database level using Row-Level Security (RLS) in PostgreSQL.

How to implement: Create a Hibernate @FilterDef and @Filter on your entities. Enable the filter in your repository or service layer. For native queries, you're on your own — wrap them in a repository that appends the tenant condition.

Hibernate filters work, but they're not applied to @OneToMany or @ManyToMany collections by default unless you explicitly set @FilterJoinTable. Also, Hibernate filters don't work with JOIN FETCH in JPQL — you need to use @QueryHint or write the filter manually.

Here's the entity setup with Hibernate filter.

Invoice.javaJAVA
1
@Entity\n@Table(name = "invoices")\n@FilterDef(name = "tenantFilter",\n        parameters = @ParamDef(name = "tenantId", type = String.class))\n@Filter(name = "tenantFilter",\n        condition = "tenant_id = :tenantId")\npublic class Invoice {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n\n    @Column(name = "tenant_id", nullable = false)\n    private String tenantId;\n\n    private BigDecimal amount;\n\n    // getters and setters\n}
Output
All queries on Invoice will automatically include `WHERE tenant_id = :currentTenantId`.
💡Native Queries Are a Liability
📊 Production Insight
Set up a database trigger that rejects INSERT/UPDATE if tenant_id doesn't match the session variable. This is your last line of defense against application-level bugs.
🎯 Key Takeaway
Discriminator column is fast and simple but dangerous. Use it only when isolation is not a compliance requirement, and always enforce tenant filtering at the database level with RLS.

Handling Tenant Provisioning and Schema Migrations

When a new tenant signs up, you need to provision their storage — create a database, schema, or just insert a row. This is where most tutorials stop, but in production, this is where things break.

For database-per-tenant: You need to create a new database, run Flyway migrations against it, and register the datasource in your application. Do this asynchronously because creating a database and running migrations can take seconds. The tenant should see a 'provisioning' state until migrations complete.

For schema-per-tenant: Create the schema using CREATE SCHEMA IF NOT EXISTS tenant_123, then run Flyway migrations targeted at that schema. Flyway's schemas property can be set per migration.

For discriminator column: No provisioning needed — just insert the first record with the tenant's ID. But you still need to ensure the tenant exists in a tenants table for validation.

Here's a provisioning service that handles schema-per-tenant with Flyway. Note the use of a separate DataSource for migrations so you don't interfere with the main connection pool.

TenantProvisioningService.javaJAVA
1
@Service\npublic class TenantProvisioningService {\n\n    private final DataSource migrationDataSource;\n\n    public void provisionSchema(String tenantId) {\n        String schemaName = "tenant_" + tenantId;\n        try (Connection conn = migrationDataSource.getConnection();\n             Statement stmt = conn.createStatement()) {\n            stmt.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);\n        } catch (SQLException e) {\n            throw new ProvisioningException("Failed to create schema", e);\n        }\n\n        Flyway flyway = Flyway.configure()\n                .dataSource(migrationDataSource)\n                .schemas(schemaName)\n                .locations("db/migration/tenant")\n                .load();\n        flyway.migrate();\n    }\n}
Output
Creates a new schema and runs Flyway migrations on it.
🔥Migration Location Matters
📊 Production Insight
Add a provisioning queue with retry logic. If schema creation fails mid-migration, you don't want to leave the tenant in a half-provisioned state. Use a database transaction to track provisioning state.
🎯 Key Takeaway
Provisioning must be asynchronous and idempotent. Use a separate datasource for migrations to avoid blocking the main pool.

Testing Multi-Tenant Applications

Testing multi-tenancy is harder than you think. Unit tests with @DataJpaTest use a single in-memory database, so you can't test tenant isolation properly. Integration tests need to simulate multiple tenants hitting the same endpoints.

Unit tests: Mock the TenantContext to return a fixed tenant ID. Test that queries include the tenant filter. Use AssertJ to verify SQL logs.

Integration tests with Testcontainers: Spin up a PostgreSQL container, create two schemas (tenant_a, tenant_b), insert test data for both, then verify that Tenant A's API call doesn't return Tenant B's data. This catches the most common bug: missing tenant filter on a join.

Performance tests: Simulate 50 tenants with 10 concurrent requests each. Monitor connection pool usage and response times. If you see spikes when a new tenant is provisioned, your pre-warming logic is broken.

Here's a Testcontainers-based test that verifies tenant isolation for schema-per-tenant.

MultiTenantIsolationTest.javaJAVA
1
@Testcontainers\n@SpringBootTest\nclass MultiTenantIsolationTest {\n\n    @Container\n    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");\n\n    @Autowired\n    private InvoiceRepository invoiceRepository;\n\n    @Test\n    void testTenantIsolation() {\n        TenantContext.setCurrentTenantId("tenant_a");\n        invoiceRepository.save(new Invoice("tenant_a", BigDecimal.valueOf(100)));\n\n        TenantContext.setCurrentTenantId("tenant_b");\n        invoiceRepository.save(new Invoice("tenant_b", BigDecimal.valueOf(200)));\n\n        TenantContext.setCurrentTenantId("tenant_a");\n        List<Invoice> invoicesForA = invoiceRepository.findAll();\n        assertThat(invoicesForA).hasSize(1);\n        assertThat(invoicesForA.get(0).getAmount()).isEqualByComparingTo("100");\n    }\n}
Output
Test passes: Tenant A sees only their invoice, not Tenant B's.
⚠ Don't Forget to Clean TenantContext in Tests
📊 Production Insight
Add a CI pipeline step that runs multi-tenant isolation tests with randomized tenant IDs. I once found a bug that only manifested when tenant IDs had hyphens.
🎯 Key Takeaway
Integration tests with Testcontainers are essential for catching tenant isolation bugs. Unit tests alone won't save you.

Production Debugging: Common Failures and Fixes

After years of running multi-tenant systems in production, here are the most common failures I've seen — and how to fix them fast.

1. Connection leaks: A developer forgets to close a JDBC connection in a native query. With database-per-tenant, each leak consumes a connection from that tenant's pool. Eventually, the pool is exhausted and that tenant's users see errors. Fix: use HikariCP's leakDetectionThreshold (set it to 5 seconds in development, 30 seconds in production). It logs a stack trace when a connection is held too long.

2. Schema drift: In schema-per-tenant, you run migrations for each schema. If one schema's migration fails and you don't retry, that tenant's schema becomes out of sync with the others. Fix: use Flyway's outOfOrder mode and run a daily reconciliation job that checks all schemas are at the same version.

3. ThreadLocal leaks in async code: When you use @Async or a TaskExecutor, the new thread doesn't inherit the ThreadLocal. The async method runs with a null tenant ID, which either fails or queries the default schema. Fix: use a custom TaskDecorator that copies the tenant ID from the parent thread to the worker thread.

Here's a TaskDecorator that propagates the tenant context to async threads.

TenantAwareTaskDecorator.javaJAVA
1
public class TenantAwareTaskDecorator implements TaskDecorator {\n\n    @Override\n    public Runnable decorate(Runnable task) {\n        String tenantId = TenantContext.getCurrentTenantId();\n        return () -> {\n            try {\n                TenantContext.setCurrentTenantId(tenantId);\n                task.run();\n            } finally {\n                TenantContext.clear();\n            }\n        };\n    }\n}
Output
Async tasks now execute with the correct tenant context.
💡Async Without Tenant Context = Data Corruption
📊 Production Insight
Add a health check that queries each tenant's database with a simple SELECT 1. If a tenant's database is down, remove them from the routing table so the app doesn't hang.
🎯 Key Takeaway
Always propagate tenant context to async threads using a custom TaskDecorator. Set leakDetectionThreshold on your connection pools.
● Production incidentPOST-MORTEMseverity: high

The Connection Pool That Ate Our Database

Symptom
Application becomes unresponsive every day at 10 AM. Database CPU at 100%. Connection timeouts spike. Tenants report 503 errors.
Assumption
We assumed HikariCP's default pool size of 10 per tenant was fine. 200 tenants × 10 connections = 2000 connections. MySQL max_connections was 500.
Root cause
We created a new HikariDataSource per tenant without limiting total connections. The database server hit max_connections and started rejecting all new connections, including health checks.
Fix
Switched to a shared connection pool with a tenant-aware routing datasource. Used HikariCP's max-lifetime and minimum-idle settings per tenant but capped total pool size at 400. Added circuit breaker to fail fast when pool is exhausted.
Key lesson
  • Never create separate connection pools per tenant without a global cap.
  • Monitor database connection counts in production — set alerts at 80% of max_connections.
  • Use a connection pool that supports tenant routing, like HikariCP with a custom DataSource.
Production debug guideQuick reference for common multi-tenancy failures3 entries
Symptom · 01
Tenant A sees Tenant B's data
Fix
Check if ThreadLocal is cleared in finally block. Verify Hibernate filter is applied to all queries, especially native queries and @OneToMany collections.
Symptom · 02
Connection pool exhaustion
Fix
Check HikariCP's leakDetectionThreshold logs. Look for connections held longer than 30 seconds. Verify you're not creating new datasources per request.
Symptom · 03
New tenant gets 503 errors
Fix
Check if datasource was pre-warmed. If not, the first request blocks while HikariCP initializes. Add async provisioning with a health check.
★ Multi-Tenancy Quick Debug Cheat SheetFive-minute fixes for the most common multi-tenancy issues
Data leak between tenants
Immediate action
Add a database trigger that rejects queries without a tenant session variable.
Commands
SELECT current_setting('app.tenant_id')
SHOW VARIABLES LIKE 'app.tenant_id'
Fix now
Set a mandatory session variable in the connection pool init SQL.
Async task runs with wrong tenant+
Immediate action
Check if TaskDecorator is configured in ThreadPoolTaskExecutor.
Commands
grep -r "@Async" src/
grep -r "TaskDecorator" src/
Fix now
Implement TenantAwareTaskDecorator and register it in the executor bean.
Schema migration fails for one tenant+
Immediate action
Check Flyway's schema_version table for that tenant.
Commands
SELECT * FROM tenant_123.schema_version ORDER BY installed_rank DESC LIMIT 1;
SELECT version, success FROM tenant_123.schema_version WHERE success = false;
Fix now
Run Flyway repair for that schema, then re-run migration.
StrategyIsolationConnection PoolOperational CostCompliance
Database-per-tenantStrongestMultiple pools (risk of explosion)High (backup each DB)PCI DSS, HIPAA
Schema-per-tenantStrongSingle poolMedium (migrate per schema)SOC 2
Discriminator columnWeakSingle poolLowNone
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
TenantAwareEntity.java@MappedSuperclass\npublic abstract class TenantAwareEntity {\n\n @Column(name...Understanding Multi-Tenancy
TenantContextFilter.java@Component\npublic class TenantContextFilter extends OncePerRequestFilter {\n\n ...What the Official Docs Won't Tell You
TenantDataSourceProvider.java@Component\npublic class TenantDataSourceProvider implements MultiTenantConnecti...Strategy 1
SchemaPerTenantConnectionProvider.java@Component\npublic class SchemaPerTenantConnectionProvider implements MultiTenan...Strategy 2
Invoice.java@Entity\n@Table(name = "invoices")\n@FilterDef(name = "tenantFilter",\n p...Strategy 3
TenantProvisioningService.java@Service\npublic class TenantProvisioningService {\n\n private final DataSour...Handling Tenant Provisioning and Schema Migrations
MultiTenantIsolationTest.java@Testcontainers\n@SpringBootTest\nclass MultiTenantIsolationTest {\n\n @Conta...Testing Multi-Tenant Applications
TenantAwareTaskDecorator.javapublic class TenantAwareTaskDecorator implements TaskDecorator {\n\n @Overrid...Production Debugging

Key takeaways

1
Choose your multi-tenancy strategy based on compliance needs and operational budget
database-per-tenant for fintech, schema-per-tenant for most B2B SaaS, discriminator for internal tools only.
2
Always clean ThreadLocal in a finally block to prevent tenant data leaks between requests.
3
Use a shared connection pool with tenant routing for database-per-tenant to avoid connection pool explosion.
4
Test tenant isolation with Testcontainers
unit tests alone cannot catch all bugs.
5
Propagate tenant context to async threads using a custom TaskDecorator.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how ThreadLocal can cause data leaks in a multi-tenant Spring Bo...
Q02SENIOR
What is the difference between MultiTenantConnectionProvider and Current...
Q03SENIOR
How would you test that Tenant A cannot access Tenant B's data in a sche...
Q01 of 03SENIOR

Explain how ThreadLocal can cause data leaks in a multi-tenant Spring Boot app and how to prevent it.

ANSWER
ThreadLocal stores the tenant ID per thread. If you don't clear it in a finally block, the next request on the same thread (from Tomcat's thread pool) will see the previous tenant's ID. Prevent this by using OncePerRequestFilter with a try-finally block that clears the ThreadLocal after every request.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Which multi-tenancy strategy is best for a fintech SaaS startup?
02
How do I handle tenant-specific configuration like timezone or currency?
03
Can I mix strategies — some tenants on dedicated databases, others on shared schema?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

6 min read · try the examples if you haven't

Previous
SSL and TLS Configuration in Spring Boot
39 / 121 · Spring Boot
Next
Spring Modulith: Modular Monolith Architecture with Spring