Tenant Context Core
Latest stable Based on Bluetape4k release 2.0.0
Problem
Section titled “Problem”Common APIs and ThreadLocal/ScopedValue carriers for explicit tenant binding in JDK 25 applications. There is no default tenant or fallback. This manual connects that purpose to the current build, source entry points, tests, configuration resources, and lifecycle evidence instead of duplicating the README feature list.
When to use
Section titled “When to use”Use bluetape4k-tenant when an application must carry an authenticated, canonical TenantId through a lexical scope without inventing a default tenant. Choose ThreadLocalTenantContext for synchronous work that remains on one platform thread and ScopedValueTenantContext for virtual-thread or structured-concurrency code. Use a carrier-specific adapter for Reactor, Ktor, or coroutine boundaries instead of assuming automatic propagation.
Coordinates
Section titled “Coordinates”dependencies { implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:<version>")) implementation("io.github.bluetape4k:bluetape4k-tenant")}Gradle project path: :bluetape4k-tenant. Source directory: bluetape4k/tenant.
Concepts
Section titled “Concepts”The first source-level concepts to inspect are MissingTenantContextException, ScopedValueTenantContext, TenantContext, TenantId, and ThreadLocalTenantContext. File names are navigation anchors; read each declaration and its tests before treating it as a public contract.
Quick start
Section titled “Quick start”Create one application-scoped carrier and use only the lexical withTenant API:
val tenantContext: TenantContext = ThreadLocalTenantContext()
tenantContext.withTenant(TenantId("clinic-a")) { repository.findAppointments(tenantContext.requireCurrent())}Map raw headers or tokens to an authorized domain value before constructing TenantId.
API by task
Section titled “API by task”| Entry point | What to verify |
|---|---|
TenantContext | Bind with withTenant, query optionally with currentOrNull, or require a binding with requireCurrent. |
ThreadLocalTenantContext | Keep synchronous platform-thread bindings lexical; nested scopes restore the previous value in finally. |
ScopedValueTenantContext | Use JDK 25 ScopedValue lexical inheritance with virtual threads and StructuredTaskScope. |
TenantId | Carry only a canonical application value after authentication and authorization. |
MissingTenantContextException | Handle a missing required binding at the application boundary that owns status or error mapping. |
Patterns
Section titled “Patterns”Inject one carrier instance into the boundary and downstream components that share the binding. Keep authentication, authorization, tenant existence checks, schema or connection routing, and persistence outside this module. Do not expose mutable set or clear operations or create a carrier per request.
Integrations
Section titled “Integrations”The module build declares no direct api, implementation, compileOnly, or runtimeOnly dependency line. Inspect plugins and generated metadata in the build file.
Configuration
Section titled “Configuration”The module has no configuration properties or resources. Carrier choice and lifecycle are explicit application decisions made when the TenantContext instance is constructed and injected.
Failures
Section titled “Failures”currentOrNull() returns null when unbound, while requireCurrent() throws MissingTenantContextException("Tenant context is not bound"). ThreadLocalTenantContext restores a nested previous value and removes the binding in finally; ScopedValueTenantContext keeps the binding inside its lexical carrier. There is no fallback tenant.
Operations
Section titled “Operations”Do not put raw headers, tokens, or tenant values in logs, exceptions, MDC, or metric labels. If a consumer records binding failures, use bounded carrier/stage labels and existing correlation or trace identifiers without exposing tenant identity.
Testing
Section titled “Testing”Run the module test task:
./gradlew :bluetape4k-tenant:test --no-configuration-cacheRepresentative test anchors:
ScopedValueTenantContextTestTenantContextApiTestTenantContextRetentionStressTestTenantIdTestThreadLocalTenantContextTest
Workshops
Section titled “Workshops”No dedicated workshop path is registered in the manual manifest. Use the module README and the representative tests above as runnable evidence.
Limitations
Section titled “Limitations”The module does not authenticate tenants, authorize access, select schemas or connections, or propagate bindings across coroutine suspension and dispatcher hops. Independently started virtual threads also do not inherit a binding automatically. Applications must select the correct adapter at each asynchronous or framework boundary.