ObjectStackObjectStack

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 removed

has

Checks if a key exists without retrieving the value.

const exists = await cacheService.has('user:usr_01HQ3V5K8N');
// true or false

clear

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 throw RedisCacheAdapter not yet implemented.

On this page