Cache Customization
Overview
TeaQL can work with project-specific caching through UserContext. The goal is not to force one cache implementation, but to give each project a clean place to control cache keys, cache scope, invalidation, and environment-specific behavior.
Cache customization is useful for:
- Frequently loaded reference data
- Permission and tenant rules
- Aggregation results
- User profile and session-derived data
- Expensive domain lookups
- Read-heavy query fragments
What Can Be Customized
Common cache customization points include:
| Area | Examples |
|---|---|
| Cache provider | In-memory cache, Redis, Caffeine, custom distributed cache |
| Key strategy | Tenant-aware keys, user-aware keys, locale-aware keys |
| TTL policy | Short-lived query cache, long-lived reference cache |
| Invalidation | Entity save, delete, audit event, manual refresh |
| Scope | Request, user, tenant, global |
| Fallback | Cache miss loading, stale value handling, fail-open vs fail-closed |
Recommended Design
Keep cache access behind CustomUserContext:
Business logic
-> CustomUserContext cache method
-> project cache service
-> cache provider
This prevents generated entities and services from depending directly on Redis, Caffeine, or any other cache provider.
Example Cache Service
public interface TeaQLCacheService {
<T> T get(String key, Class<T> type);
void put(String key, Object value, Duration ttl);
void evict(String key);
void evictByPrefix(String prefix);
}
Expose it through CustomUserContext:
public class CustomUserContext extends UserContext {
public TeaQLCacheService cacheService() {
return getBean(TeaQLCacheService.class);
}
public String tenantCacheKey(String namespace, String id) {
return currentTenantCode() + ":" + namespace + ":" + id;
}
}
Caching Reference Data
Example:
public Currency loadCurrency(String currencyCode) {
String key = tenantCacheKey("currency", currencyCode);
Currency cached = cacheService().get(key, Currency.class);
if (cached != null) {
return cached;
}
Currency currency = Q.currencies()
.filterByCode(currencyCode).comment("Query currencies").purpose("Load data")
.executeForOne(this);
cacheService().put(key, currency, Duration.ofHours(6));
return currency;
}
This keeps the query readable and makes tenant-aware cache behavior explicit.
Request-Scoped Cache
Some values should only live during the current request:
public Object requestCache(String key, Supplier<Object> loader) {
Object value = getLocalStorage(key);
if (value != null) {
return value;
}
Object loaded = loader.get();
putLocalStorage(key, loaded);
return loaded;
}
Use request-scoped cache for:
- Current user profile
- Permission matrix
- Parsed request metadata
- Repeated lookups inside one service call
Invalidation on Writes
Cache invalidation should be tied to entity changes.
Example pattern:
public void afterSave(Object entity) {
if (entity instanceof Customer customer) {
cacheService().evict(tenantCacheKey("customer", customer.getId().toString()));
cacheService().evictByPrefix(currentTenantCode() + ":customer-list:");
}
}
Keep invalidation conservative. It is better to evict a little more data than to serve incorrect business information.
Cache Key Guidelines
A good cache key should include all inputs that affect the result:
tenant:locale:namespace:object-id:version
Include:
- Tenant
- Locale, if display text changes by language
- User or role, if permissions affect the result
- Entity version, if cached objects must follow optimistic locking
- Query parameters, if caching query results
Best Practices
- Do not cache permission-sensitive results without user or role scope.
- Keep cache logic in
CustomUserContextor a cache service. - Use short TTLs for query results.
- Use longer TTLs for stable reference data.
- Evict related list caches after writes.
- Log cache behavior while tuning performance.
- Avoid caching mutable entity instances if callers may modify them.
Testing Checklist
- Cache hit returns the expected object.
- Cache miss loads from TeaQL query.
- Tenant A cannot read Tenant B cached data.
- Locale-specific values do not leak across languages.
- Save/update/delete evicts related keys.
- Cache provider failure has the expected fallback behavior.
Summary
TeaQL cache customization should be centralized through CustomUserContext. This gives each project full control over provider choice, key design, TTL, invalidation, and request-scoped optimization without polluting domain logic.