Problem
Multi-tenancy gets re-implemented on every project, and the bugs are always the same: a missed tenant filter, a cache key without a tenant prefix, a background job running against the wrong schema. Manual datasource routing is error-prone, and the failure mode — cross-tenant data leakage — is the worst kind an enterprise product can have.
Business Context
Extracted from the tenancy layer of a production multi-tenant SaaS. The bet: if isolation is the default rather than a discipline, whole categories of bugs disappear. The starter targets teams building B2B SaaS on Spring Boot who need strong isolation without running a database per customer.
Architecture
Request → Tenant resolution (header | subdomain | custom)
→ TenantContext (ThreadLocal)
→ Routing DataSource (schema-per-tenant)
→ Execute → Clear context
An auto-configured servlet filter resolves the tenant, binds it to a context holder, and a
routing DataSource selects the tenant's schema for the duration of the request. A
TaskDecorator copies the context into pooled worker threads so @Async work stays correctly
scoped — and clears it afterward, because a stale ThreadLocal on a pooled thread is itself a
leakage bug.
Technology
Java 17, Spring Boot auto-configuration, Spring Data JPA, PostgreSQL schemas. Ships with three tenant-resolution strategies (header, subdomain, custom resolver) and a Docker Compose harness for local multi-schema testing.
Challenges
The hard problem was context propagation across async boundaries. ThreadLocals don't follow work
onto pooled executor threads, so @Async methods silently ran without a tenant. The alternatives
were passing the tenant explicitly through every method signature (invasive) or decorating every
executor (one-time setup). The TaskDecorator approach won, with an aggressive "fail closed"
stance: no tenant in context means an exception, never a default schema.
Solution
Add the dependency, choose a resolution strategy, done. Schema-per-tenant was chosen over row-level tenancy (stronger isolation, no forgettable predicate) and database-per-tenant (operationally heavier); the trade-off is migration tooling across many schemas, which the roadmap addresses with Flyway multi-schema helpers.
Results
- Isolation by default — services built on the starter have had zero cross-tenant incidents.
- New services get tenancy in minutes instead of days.
- Async and scheduled work is tenant-safe without per-call ceremony.
Lessons Learned
ThreadLocal context needs deliberate propagation in async code — and deliberate cleanup. Fail-closed defaults matter more than flexibility when the failure mode is data leakage.