Skip to main content

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:

AreaExamples
Cache providerIn-memory cache, Redis, Caffeine, custom distributed cache
Key strategyTenant-aware keys, user-aware keys, locale-aware keys
TTL policyShort-lived query cache, long-lived reference cache
InvalidationEntity save, delete, audit event, manual refresh
ScopeRequest, user, tenant, global
FallbackCache miss loading, stale value handling, fail-open vs fail-closed

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 CustomUserContext or 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.