Tenant Resolver & Caching
The Tenant Resolver system handles multi-tenant resolution with a cache-first architecture. Its goal is to resolve which tenant a request belongs to with minimal database load.
Architecture Overview
The system uses a three-layer architecture:
| Layer | Responsibility |
|---|---|
TenantServiceProvider | Request bootstrap, domain extraction |
TenantResolverService | Routing, short-circuit optimization |
TenantResolverUnifiedQueryService | Cache management, query execution |
CurrentTenant | Singleton container, app configuration |
Request Lifecycle
Resolution happens during Laravel's boot phase, before any controller code runs. The TenantServiceProvider extracts the domain from the request, resolves the tenant (using cache when possible), and configures the application.
The tenant() helper returns the current tenant instance from the CurrentTenant singleton. All application code uses this to access the resolved tenant.
Caching Strategy
Two-Tier Caching
The system uses a domain pointer pattern:
domain:example.com → "42" (pointer to ID)
id:42 → {full payload} (actual data)
Why this design?
- IDs are stable, domains change. When a tenant updates their domain, only the pointer needs updating.
- Many domains can point to one tenant. Custom domains, subdomains, and aliases all resolve to the same payload.
- Single source of truth. The ID-based cache is authoritative, simplifying invalidation logic.
TTL Randomization
Cache entries expire with a randomized TTL (700-1000 seconds).
Why randomize? After a deploy or cache flush, all workers would otherwise hit the database simultaneously when cache expires. Randomized TTL spreads the refresh across time, preventing load spikes.
Lock Mechanism
When cache misses occur, a lock prevents multiple workers from executing the same query:
- First worker acquires lock, queries database, stores result
- Other workers wait (up to 3 seconds) for the lock
- When lock releases, waiting workers use the cached result
- If lock times out, worker queries independently (graceful degradation)
Why locks? Without them, a cache miss on a popular tenant could trigger hundreds of identical queries from concurrent workers.
Unified Payload
The cache stores a complete tenant payload in a single entry:
| Data | Why Included |
|---|---|
Tenant | Core tenant record |
TenantMeta | Extended settings and configuration |
SalesSettings | Payment and commerce configuration |
SignUpIntent | Plan and billing information |
Features | Pre-computed feature flags and limits |
Why unified?
- Eliminates N+1 queries. A single JOIN query fetches everything instead of separate queries per relation.
- Single cache entry. One cache hit returns all tenant data needed for request handling.
- Features pre-computed. The Features System values are computed at cache time, making feature checks instant.
Cache Invalidation
Invalidation happens automatically via Laravel model observers:
| When | What's Invalidated |
|---|---|
| Tenant saved/deleted | ID cache + all domain pointers |
| TenantMeta saved/deleted | ID cache (via tenant lookup) |
| SalesSettings saved/deleted | ID cache (via tenant lookup) |
| SignUpIntent saved/deleted | ID cache (via tenant lookup) |
| Plan/Tenant features changed | ID cache (via tenant lookup) |
Domain pointer healing: When a domain pointer references an expired or flushed ID cache, the next request triggers a fresh query and repopulates both caches.
Manual invalidation: Flush via the unified query service to force cache refresh.
Components Reference
| Component | Location | Purpose |
|---|---|---|
TenantServiceProvider | app/Providers/ | Boots tenant resolution on every request; extracts domain from Host header or X-Farfalla-Tenant-Id |
TenantResolverService | app/Domains/Shared/Services/ | Routing layer with short-circuit optimization; returns current tenant immediately if domain matches |
TenantResolverUnifiedQueryService | app/Domains/Shared/Services/ | Execution engine; manages cache, locks, unified queries, and payload hydration |
CurrentTenant | app/ | Singleton holding resolved tenant; configures app URL, locale, cache prefix, and storage paths |