ICacheService Contract
Reference for the Cache Service contract — key-value caching with TTL support
ICacheService Contract
The Cache Service provides a key-value caching layer used throughout ObjectStack for metadata caching, query result caching, and rate limiting. Implementations can range from in-memory Maps to Redis clusters. It does not store sessions: the session of record is always the sys_session table, because ADR-0069 D4's session controls (idle timeout, absolute lifetime, concurrent-session cap) revoke a session by writing that row. A host may opt in explicitly with cacheSecondaryStorage() from @objectstack/plugin-auth, but should know what it buys: better-auth then answers session lookups from the cached snapshot without reading the database, so opting in disables all three D4 session controls — a revoked session stays usable until its cached copy expires.
Source: packages/spec/src/contracts/cache-service.ts
Service name: cache — resolve with kernel.getService('cache')
Interface Definition
export interface ICacheService {
// Core Operations
get<T = unknown>(key: string): Promise<T | undefined>;
set<T = unknown>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
has(key: string): Promise<boolean>;
clear(): Promise<void>;
// Observability
stats(): Promise<CacheStats>;
}CacheStats is returned by stats() for monitoring:
export interface CacheStats {
hits: number; // Total number of cache hits
misses: number; // Total number of cache misses
keyCount: number; // Number of keys currently stored
memoryUsage?: number; // Memory usage in bytes (if available)
}Core Operations
get
Retrieves a cached value by key. Returns undefined if the key does not exist or has expired.
const user = await cacheService.get<AuthUser>('user:usr_01HQ3V5K8N');
if (user) {
console.log(user.name); // "Jane Smith"
}set
Stores a value with an optional TTL (time-to-live), passed as a positional argument in seconds.
// Cache for 5 minutes
await cacheService.set('user:usr_01HQ3V5K8N', userData, 300);
// Cache indefinitely (omit the ttl argument)
await cacheService.set('config:feature_flags', flags);delete
Removes a key from the cache. Returns true if the key existed.
const deleted = await cacheService.delete('user:usr_01HQ3V5K8N');
// true if the key was found and removedhas
Checks if a key exists without retrieving the value.
const exists = await cacheService.has('user:usr_01HQ3V5K8N');
// true or falseclear
Removes all entries from the cache. clear() takes no arguments — it flushes the entire store.
// Clear every key in the cache
await cacheService.clear();The contract has no pattern-based clearing, TTL inspection, bulk, atomic, or namespace
operations. To scope keys, prefix them yourself (e.g. meta:object:task) and track which
keys to invalidate at the call site.
Observability
stats
Returns counters for monitoring cache effectiveness.
const { hits, misses, keyCount } = await cacheService.stats();
const hitRate = hits / (hits + misses || 1);
console.log(`Cache hit rate: ${(hitRate * 100).toFixed(1)}% (${keyCount} keys)`);Common Cache Patterns
Cache-Aside (Lazy Loading)
async function getObjectDefinition(name: string): Promise<ObjectDefinition> {
const cacheKey = `meta:object:${name}`;
// Try cache first
const cached = await cacheService.get<ObjectDefinition>(cacheKey);
if (cached) return cached;
// Load from source
const definition = await metadataService.getObject(name);
if (!definition) throw new Error(`Object not found: ${name}`);
// Store in cache
await cacheService.set(cacheKey, definition, 3600);
return definition;
}Write-Through
async function updateObjectDefinition(
name: string,
changes: Partial<ObjectDefinition>
): Promise<ObjectDefinition> {
// Update source of truth — register() saves the full definition
const current = await metadataService.getObject(name);
const updated = { ...(current as ObjectDefinition), ...changes };
await metadataService.register('object', name, updated);
// Update cache immediately
await cacheService.set(`meta:object:${name}`, updated, 3600);
return updated;
}Cache Invalidation on Events
// watch() is optional on IMetadataService — check before subscribing
const handle = metadataService.watch?.('object', (event) => {
// Invalidate the affected key when metadata changes.
// The contract deletes individual keys — track related keys
// (e.g. cached queries) yourself to invalidate them.
cacheService.delete(`meta:object:${event.name}`);
});Implementations
Two adapters ship in @objectstack/service-cache, both implementing ICacheService:
MemoryCacheAdapter— Map-backed in-process store with TTL expiry and optional LRU-style eviction (maxSize). Suitable for development, testing, and single-process deployments.RedisCacheAdapter— a skeleton placeholder for future Redis integration; its methods currently throwRedisCacheAdapter not yet implemented.