Tenant Context Propagation
The TenantContextHolder is the central mechanism that ensures every operation executes within the correct tenant boundary. It uses thread-local storage (Java) or context variables (Python) to carry tenant identity through the entire request processing chain.
Java Implementation
TenantContextHolder
public final class TenantContextHolder {
private static final ThreadLocal<TenantContext> CONTEXT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CONTEXT.set(new TenantContext(tenantId, null, Map.of()));
}
public static Optional<String> getTenantId() {
return getContext().map(TenantContext::tenantId);
}
public static String requireTenantId() {
return requireContext().tenantId();
}
public static void clear() {
CONTEXT.remove();
}
}TenantContext Record
public record TenantContext(
String tenantId,
String userId,
Map<String, Object> attributes
) {
public TenantContext {
if (tenantId == null || tenantId.isBlank()) {
throw new IllegalArgumentException("Tenant ID cannot be null or blank");
}
attributes = attributes != null ? Map.copyOf(attributes) : Map.of();
}
}Context Setting
The context is set from three sources:
| Source | Scenario | Code |
|---|---|---|
| JWT token | Standard request | TenantContextHolder.setTenant(tenantId, userId) |
| HTTP header | Inter-service call | Extract X-Tenant-ID from request |
| Programmatic | Background jobs | TenantContextHolder.setContext(new TenantContext(...)) |
Context Propagation to Child Threads
When work is dispatched to thread pools, the context must be explicitly propagated:
// Wrap a Runnable to preserve context
Runnable wrapped = TenantContextHolder.wrapWithContext(() -> {
processInBackground();
});
executorService.submit(wrapped);The wrapper captures the context at wrap time and restores it at execution time.
Python Implementation
Python services use contextvars for async-compatible context:
from contextvars import ContextVar
_tenant_id: ContextVar[str] = ContextVar('tenant_id')
def get_tenant_id() -> str:
return _tenant_id.get()
def set_tenant_id(tenant_id: str) -> None:
_tenant_id.set(tenant_id)Python's contextvars automatically propagate through asyncio task chains.
Inter-Service Propagation
Tenant context propagates via HTTP headers:
| Header | Purpose |
|---|---|
X-Tenant-ID | Tenant identifier |
X-User-ID | Authenticated user |
For Kafka events, context propagates as message headers and payload fields.
Security Invariants
| Invariant | Enforcement |
|---|---|
| No context = no data access | requireTenantId() throws IllegalStateException |
| Context always cleared | finally block in filter chain calls clear() |
| Queries always filtered | Repository uses requireTenantId() for every query |
| Cross-tenant access logged | Mismatch triggers security alert |
Related Pages
- Database Isolation -- How context drives database scoping
- Namespace Isolation -- Kubernetes boundaries
- Security: Tenant Context -- Security-focused context details