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.