Building a multi-tenant SaaS with Spring Boot and ThreadLocal isolation
Rahul Verma
· 11 min read
Multi-tenancy is one of those problems that's easy to get approximately right and dangerous to get slightly wrong. A single missed WHERE clause can leak one company's invoices into another's dashboard. Here's the approach we settled on for the backend.
A shared schema with a tenant column
We use a shared-schema, shared-database model: every tenant-scoped table carries a workspace/tenant identifier column, rather than provisioning a separate schema or database per customer. This keeps migrations simple and lets us run one Flyway migration pipeline across all tenants instead of one per tenant.
TenantContext and ThreadLocal
A request-scoped TenantContext, backed by a ThreadLocal, is populated as soon as an authenticated request is resolved to a workspace, and cleared at the end of the request. Repository and service code reads the current tenant from this context rather than having it passed explicitly through every method signature, which keeps call sites clean.
The risk with ThreadLocal is leakage across requests if it isn't cleared — we address this with a servlet filter that guarantees cleanup in a finally block, and tests that assert the context is empty between requests.
Defense in depth: Hibernate filters
TenantContext scopes queries at the service layer, but we also apply a Hibernate tenant filter at the ORM level as a second line of defense, so that even a query written without an explicit tenant check is automatically scoped. The goal is that a single missing line of application code should not be enough to leak data across tenants.
