Cache Wiring
Recipes are working wiring examples with the sharp edges annotated. This page and the
cache-wiringrecipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.
Two packages compose:
@modularityjs/cache—CacheServiceabstract contract +CacheModule. Methods:get<T>(key) → T | undefined,set<T>(key, value, options?: { ttlMs?, tags? }),has(key) → boolean,delete(key),invalidateTag(tag),invalidateTags(tags[]), and atomic operationscompareAndSet<T>(key, expected, next, options?) → boolean,increment(key, delta?, options?) → number,decrement(key, delta?, options?) → number. Atomic ops are race-free across instances when the active driver iscache-redis(Lua scripts) and within a process forcache-memory. Use them for counters, single-use guards, brute-force attempt caps, optimistic swaps; tags are ignored on atomic operations.@modularityjs/cache-{memory,redis}— driver. Memory uses an in-process Map; Redis goes throughRedisModule(also required in the modules array for the Redis driver). Both bind viapreferences: [{ provide: CacheService, useClass: ... }].
get returns T | undefined — missing keys are a routine branch, not an error. set is fire-and-forget: no return value, no confirmation. ttlMs is per-entry; without it the entry lives until evicted or invalidated.
Tags are the cache-invalidation primitive. set(key, value, { tags: ['user:123'] }) stores the key under the listed tags; invalidateTag('user:123') evicts every key tagged that way in one call. Use tags when one logical resource maps to many cache keys (the same user appearing on multiple list views) — otherwise key-by-key delete is simpler.
import { CacheModule, CacheService } from '@modularityjs/cache';
import { CacheMemoryModule } from '@modularityjs/cache-memory';
import { Inject, Injectable } from '@modularityjs/di';
import { inversify } from '@modularityjs/di-inversify';
import { createApp, Module, ModularityModule } from '@modularityjs/modularity';
@Injectable()
class UserProfileService {
constructor(@Inject(CacheService) private readonly cache: CacheService) {}
async load(userId: string): Promise<UserProfile> {
const key = `users:${userId}:profile`;
const cached = await this.cache.get<UserProfile>(key);
if (cached) return cached;
const profile = await this.fetchFromDb(userId);
await this.cache.set(key, profile, {
ttlMs: 60_000,
tags: [`user:${userId}`],
});
return profile;
}
async invalidate(userId: string): Promise<void> {
await this.cache.invalidateTag(`user:${userId}`);
}
private async fetchFromDb(_userId: string): Promise<UserProfile> {
return { name: 'placeholder' };
}
}
interface UserProfile {
name: string;
}
@Module({
name: 'user-profiles',
imports: [CacheModule],
providers: [UserProfileService],
})
class AppModule {}
const app = await createApp({
di: inversify,
modules: [ModularityModule, CacheModule, CacheMemoryModule, AppModule],
});Switch to Redis with one line. Replace CacheMemoryModule with RedisModule.forRoot({ url: '...' }), CacheRedisModule — UserProfileService is unchanged. (Redis needs its own contract module imported because the driver depends on RedisModule.)
Eviction in the memory driver. Without CacheMemoryModule.forRoot({ evictionIntervalMs }), expired entries stay in memory until next accessed (lazy sweep on get). For a long-running process with high churn, configure an interval (e.g. 30_000) so the periodic sweep reclaims memory.