Skip to main content
The @ooneex/cache component is a multi-backend caching layer. Every backend implements the same ICache interface — get, set, delete, deleteByPrefix, has, clear — so you can start on the filesystem and move to Redis without changing a single call site. Values are JSON-serialized automatically and support TTL expiration.

Why this component

  • One interface, many backends. Filesystem and Redis both implement ICache — swap backends without touching callers.
  • Type-safe values. get<T>() and set<T>() carry your value type through serialization.
  • TTL built in. Set a per-key time-to-live in seconds; expired entries return undefined.
  • Bulk operations. deleteByPrefix() clears related keys in one call; clear() empties the cache.
  • Container-managed. Register a cache class with a decorator and resolve it from the container.

How it works

You pick (or implement) a backend and register it. Reads and writes go through the ICache methods; the backend handles serialization, namespacing, and expiration.
MethodPurpose
get<T>(key)Return the value or undefined if missing/expired.
set<T>(key, value, ttl?)Store a value, optionally expiring after ttl seconds.
delete(key)Remove one key; returns whether it existed.
deleteByPrefix(prefix)Remove every key starting with prefix; returns the count.
has(key)Whether a live (non-expired) key exists.
clear()Remove everything.
The built-in backends differ in where data lives, not in how you call them:
BackendBest forDistribution
FilesystemCacheLocal development, single-instance cachesOne machine (disk)
RedisCacheProduction caches shared across instancesShared (TCP)
A TTL of 0 (or omitted) means the entry never expires. Redis backends namespace every key (default cache:) to avoid collisions on a shared instance.

Environment variables

VariableBackendRequiredPurpose
CACHE_REDIS_URLRedisCacheYes (unless passed in config)Connection string, e.g. redis://localhost:6379. Missing throws CacheException (URL_REQUIRED).
FilesystemCache needs no environment variables; it defaults to a .cache directory in the working directory.
CACHE_REDIS_URL=redis://localhost:6379

Usage

The API is the same regardless of backend.
import { RedisCache } from "@ooneex/cache";
import { container } from "@ooneex/container";

const cache = container.get(RedisCache);

// Store with a 5-minute TTL
await cache.set("user:123", { name: "John", age: 30 }, 300);

// Read back with a type
const user = await cache.get<{ name: string; age: number }>("user:123");

// Existence and removal
await cache.has("user:123"); // true
await cache.delete("user:123"); // true

// Bulk removal by prefix
const removed = await cache.deleteByPrefix("user:"); // number cleared
A common pattern is read-through caching — serve from cache, fall back to the source, then populate:
const key = `user:${id}`;
let user = await cache.get<User>(key);

if (!user) {
  user = await userRepository.findById(id);
  await cache.set(key, user, 300); // cache for 5 minutes
}
Invalidate on writes so callers never read stale data:
await userRepository.update(id, data);
await cache.delete(`user:${id}`);
await cache.deleteByPrefix(`user:${id}:`); // related derived keys

Decorator and usage

@decorator.cache()

Registers a cache class with the container. It accepts an optional scope (defaults to singleton). Use it to register a built-in backend under your own name, or a custom backend that implements ICache.
import { decorator } from "@ooneex/cache";
import { RedisCache } from "@ooneex/cache";

@decorator.cache()
export class SessionCache extends RedisCache {}
Resolve it from the container and inject it where needed:
import { inject } from "@ooneex/container";

export class SessionService {
  constructor(@inject(SessionCache) private readonly cache: SessionCache) {}
}

Exceptions

The component throws CacheException when a backend is misconfigured or an operation fails. It carries a machine-readable key, a human-readable message, and a data object.
KeyWhen
URL_REQUIREDRedisCache is created without a connection string or CACHE_REDIS_URL.
MAX_SIZE_EXCEEDEDA FilesystemCache value exceeds the maximum file size (10MB by default).
import { CacheException } from "@ooneex/cache";

try {
  await cache.set("key", value);
} catch (error) {
  if (error instanceof CacheException) {
    logger.error(`Cache error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}

Best practices

  • Namespace keys. Use a stable scheme like user:123 and session:abc so deleteByPrefix() can target related entries.
  • Always set a TTL for volatile data. Cache derived or remote data with an explicit ttl so it can’t go stale indefinitely.
  • Invalidate on write. Delete (or overwrite) the key whenever the source changes; pair delete() with deleteByPrefix() for derived keys.
  • Pick the backend per environment. Filesystem for local/dev, Redis for shared production — the code stays identical.
  • Keep values serializable. Stored values round-trip through JSON; avoid class instances, functions, and circular references.
  • Treat the cache as optional. On a miss, fall back to the source of truth — never assume a key is present.
  • Throw CacheException with a stable key. In custom backends, keep keys constant and put variable detail in data.

CLI command

Scaffold a cache adapter and its test file with the generator. It writes the class under modules/<module>/src/cache/<Name>Cache.ts and installs @ooneex/cache if it is missing.
# Interactive: prompts for the name
ooneex cache:create

# Provide the name
ooneex cache:create --name=Session

# Target a module and overwrite
ooneex cache:create --name=Session --module=auth --override
OptionDescriptionDefault
--nameCache class name. The Cache suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
The generated class starts as an ICache stub, ready for you to implement each method against your backend:
import { CacheException, decorator } from "@ooneex/cache";
import type { ICache } from "@ooneex/cache";

@decorator.cache()
export class SessionCache implements ICache {
  public async get<T = unknown>(key: string): Promise<T | undefined> {
    throw new CacheException(`Failed to get key "${key}": Not implemented`);
  }

  public async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {
    throw new CacheException(`Failed to set key "${key}": Not implemented`);
  }

  public async delete(key: string): Promise<boolean> {
    throw new CacheException(`Failed to delete key "${key}": Not implemented`);
  }

  public async has(key: string): Promise<boolean> {
    throw new CacheException(`Failed to check if key "${key}" exists: Not implemented`);
  }
}
See cache:create for the full command reference.

Use with Claude and Codex

The generator ships a matching cache:create skill. It runs the scaffold and then guides your AI agent through completing the adapter — implementing get, set, delete, and has against a real backend and wiring up its client. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a cache adapter for user sessions backed by Redis with a 30 minute TTL.
For example, the prompt above maps to cache:create --name=Session, then implements the ICache methods against a Redis client.