Cache API
Package: @onebun/cache
Overview
OneBun provides a caching module with support for:
- In-memory cache
- Redis cache
- Module-based integration with DI
CacheModule
CacheModule is global by default — once imported in the root module, CacheService is automatically available in all submodules without explicit import. Use isGlobal: false to disable this behavior.
Basic Setup
import { Module } from '@onebun/core';
import { CacheModule, CacheType } from '@onebun/cache';
import { UserController } from './user.controller';
import { UserService } from './user.service';
// CacheModule imported once in root — CacheService available everywhere
@Module({
imports: [
CacheModule.forRoot({
type: CacheType.MEMORY, // CacheType.MEMORY or CacheType.REDIS
cacheOptions: {
defaultTtl: 300000, // Default TTL in milliseconds
},
}),
],
controllers: [UserController],
providers: [UserService],
})
export class AppModule {}
// CacheService is automatically available in all submodules
@Module({
controllers: [UserController],
providers: [UserService], // UserService can inject CacheService
})
export class UserModule {}Non-Global Mode
For multi-cache scenarios, disable global mode so each module can have its own CacheService instance:
// Root module: non-global cache
@Module({
imports: [
CacheModule.forRoot({
type: CacheType.REDIS,
isGlobal: false, // Each import creates new instance
}),
],
})
export class AppModule {}
// Feature modules must explicitly import CacheModule
@Module({
imports: [CacheModule.forFeature()],
providers: [OrderService],
})
export class OrderModule {}Redis Configuration
CacheModule.forRoot({
type: CacheType.REDIS,
cacheOptions: {
defaultTtl: 300000, // TTL in milliseconds
},
redisOptions: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
database: 0,
connectTimeout: 5000, // Connection timeout in ms
keyPrefix: 'myapp:cache:', // Key prefix for all cache keys
},
})Environment Variable Configuration
CacheService auto-initializes from environment variables. When you import CacheModule without .forRoot(), it reads all configuration from env vars — no explicit options needed.
# Cache type: 'memory' or 'redis'
CACHE_TYPE=redis
# Common options
CACHE_DEFAULT_TTL=300000 # Default TTL in ms (default: 0 = no expiry)
CACHE_MAX_SIZE=1000 # Max items for in-memory cache
CACHE_CLEANUP_INTERVAL=60000 # Cleanup interval in ms
# Redis options (only used when CACHE_TYPE=redis)
CACHE_REDIS_HOST=localhost
CACHE_REDIS_PORT=6379
CACHE_REDIS_PASSWORD=secret
CACHE_REDIS_DATABASE=0
CACHE_REDIS_CONNECT_TIMEOUT=5000
CACHE_REDIS_KEY_PREFIX=myapp:cache:Import CacheModule without .forRoot() — configuration comes entirely from env vars:
import { Module, Service, BaseService } from '@onebun/core';
import { CacheModule, CacheService } from '@onebun/cache';
// CacheModule without .forRoot() — auto-configures from env vars
@Module({
imports: [CacheModule],
providers: [MyService],
})
class AppModule {}
@Service()
class MyService extends BaseService {
constructor(private cacheService: CacheService) {
super();
}
}TIP
CacheModule must be imported at least once (in the root module) — it registers CacheService in the DI container. Since it's global by default, submodules get CacheService automatically. The difference is only whether you use .forRoot(options) (explicit config) or plain CacheModule (env-only config).
Configuration Priority
Configuration is resolved in this order (first wins):
CacheModule.forRoot()options (explicit module configuration)- Environment variables (
CACHE_*) - Default values (in-memory, no TTL)
Custom Environment Prefix
Use envPrefix to avoid collisions when running multiple cache instances:
CacheModule.forRoot({
type: CacheType.REDIS,
envPrefix: 'ORDERS_CACHE', // Uses ORDERS_CACHE_REDIS_HOST, etc.
})Redis Error Handling
If Redis connection fails during auto-initialization, CacheService automatically falls back to in-memory cache and logs a warning:
// If CACHE_TYPE=redis but Redis is unreachable:
// WARN: Failed to auto-initialize cache from environment
// INFO: In-memory cache initialized (fallback)Memory Configuration
CacheModule.forRoot({
type: CacheType.MEMORY,
cacheOptions: {
defaultTtl: 300000, // TTL in milliseconds
maxSize: 1000, // Maximum items
cleanupInterval: 60000, // Cleanup every 60 seconds (ms)
},
})CacheService
Injection
import { Service, BaseService } from '@onebun/core';
import { CacheService } from '@onebun/cache';
@Service()
export class UserService extends BaseService {
constructor(private cacheService: CacheService) {
super();
}
}Methods
get<T>()
Retrieve value from cache.
async get<T>(key: string): Promise<T | null>const user = await this.cacheService.get<User>('user:123');
if (user) {
// Cache hit
return user;
}
// Cache missset()
Store value in cache.
async set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>// With default TTL
await this.cacheService.set('user:123', user);
// With custom TTL (in seconds)
await this.cacheService.set('user:123', user, { ttl: 600 });
// No expiration
await this.cacheService.set('user:123', user, { ttl: 0 });delete()
Remove value from cache.
async delete(key: string): Promise<boolean>const deleted = await this.cacheService.delete('user:123');has()
Check if key exists.
async has(key: string): Promise<boolean>if (await this.cacheService.has('user:123')) {
// Key exists
}clear()
Clear all cache entries.
async clear(): Promise<void>await this.cacheService.clear();getMany<T>()
Get multiple values at once.
async getMany<T>(keys: string[]): Promise<Map<string, T | null>>const results = await this.cacheService.getMany<User>([
'user:1',
'user:2',
'user:3',
]);
for (const [key, user] of results) {
if (user) {
console.log(key, user.name);
}
}setMany()
Set multiple values at once.
async setMany<T>(entries: Map<string, T>, options?: CacheSetOptions): Promise<void>const users = new Map([
['user:1', user1],
['user:2', user2],
]);
await this.cacheService.setMany(users, { ttl: 300 });deleteMany()
Delete multiple keys.
async deleteMany(keys: string[]): Promise<number>const deletedCount = await this.cacheService.deleteMany([
'user:1',
'user:2',
]);Caching Patterns
Cache-Aside Pattern
@Service()
export class UserService extends BaseService {
constructor(
private cacheService: CacheService,
private repository: UserRepository,
) {
super();
}
async findById(id: string): Promise<User | null> {
const cacheKey = `user:${id}`;
// Try cache first
const cached = await this.cacheService.get<User>(cacheKey);
if (cached) {
this.logger.debug('Cache hit', { key: cacheKey });
return cached;
}
// Cache miss - fetch from database
this.logger.debug('Cache miss', { key: cacheKey });
const user = await this.repository.findById(id);
// Store in cache
if (user) {
await this.cacheService.set(cacheKey, user, { ttl: 300 });
}
return user;
}
}Cache Invalidation
@Service()
export class UserService extends BaseService {
async update(id: string, data: UpdateUserDto): Promise<User> {
const user = await this.repository.update(id, data);
// Invalidate cache
await this.cacheService.delete(`user:${id}`);
// Also invalidate related caches
await this.cacheService.delete('users:list');
return user;
}
async delete(id: string): Promise<void> {
await this.repository.delete(id);
// Invalidate all related caches
await this.cacheService.deleteMany([
`user:${id}`,
`user:${id}:posts`,
`user:${id}:settings`,
'users:list',
]);
}
}Cache Warming
@Service()
export class CacheWarmerService extends BaseService {
constructor(
private cacheService: CacheService,
private userRepository: UserRepository,
) {
super();
}
async warmUserCache(): Promise<void> {
this.logger.info('Warming user cache');
const users = await this.userRepository.findAll({ limit: 1000 });
const entries = new Map(
users.map(user => [`user:${user.id}`, user])
);
await this.cacheService.setMany(entries, { ttl: 3600 });
this.logger.info('User cache warmed', { count: users.length });
}
}Memoization
@Service()
export class ConfigService extends BaseService {
constructor(private cacheService: CacheService) {
super();
}
async getFeatureFlags(): Promise<FeatureFlags> {
const cacheKey = 'config:feature-flags';
// Very long TTL for rarely changing data
let flags = await this.cacheService.get<FeatureFlags>(cacheKey);
if (!flags) {
flags = await this.fetchFeatureFlags();
await this.cacheService.set(cacheKey, flags, { ttl: 3600 }); // 1 hour
}
return flags;
}
}Cache Types
CacheSetOptions
interface CacheSetOptions {
/** Time-to-live in seconds. 0 for no expiration */
ttl?: number;
}CacheStats
interface CacheStats {
hits: number;
misses: number;
keys: number;
size: number;
}Shared Redis Connection
For applications using both cache and WebSocket (or other Redis-based features), you can share a single Redis connection:
import { SharedRedisProvider } from '@onebun/core';
import { createRedisCache, RedisCache } from '@onebun/cache';
// Configure shared Redis at app startup
SharedRedisProvider.configure({
url: 'redis://localhost:6379',
keyPrefix: 'myapp:',
});
// Option 1: Use shared client via options
const cache = createRedisCache({
useSharedClient: true,
defaultTtl: 60000,
});
await cache.connect();
// Option 2: Pass RedisClient directly
const sharedClient = await SharedRedisProvider.getClient();
const cache = new RedisCache(sharedClient);
// Check if using shared connection
console.log(cache.isUsingSharedClient()); // trueBenefits:
- Single connection pool for cache and WebSocket
- Reduced memory footprint
- Consistent key prefixing across features
Effect.js Integration
For Effect.js-based usage:
import { createCacheModule, cacheServiceTag, CacheType } from '@onebun/cache';
import { Effect, pipe } from 'effect';
// Create service
const cacheLayer = createCacheModule({
type: CacheType.MEMORY,
cacheOptions: {
defaultTtl: 300000,
},
});
// Use in Effect
const program = pipe(
cacheServiceTag,
Effect.flatMap((cache) =>
Effect.promise(() => cache.get<User>('user:123'))
),
);
// Run
Effect.runPromise(
Effect.provide(program, cacheLayer)
);Complete Example
import { Module, Controller, BaseController, Service, BaseService, Get, Post, Delete, Param, Body } from '@onebun/core';
import { CacheModule, CacheService, CacheType } from '@onebun/cache';
import { type } from 'arktype';
// Types
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
// Service
@Service()
export class ProductService extends BaseService {
private products = new Map<string, Product>();
constructor(private cacheService: CacheService) {
super();
// Seed some data
this.products.set('1', { id: '1', name: 'Widget', price: 9.99, stock: 100 });
this.products.set('2', { id: '2', name: 'Gadget', price: 19.99, stock: 50 });
}
async findById(id: string): Promise<Product | null> {
const cacheKey = `product:${id}`;
// Check cache
const cached = await this.cacheService.get<Product>(cacheKey);
if (cached) {
this.logger.debug('Product cache hit', { id });
return cached;
}
// Fetch from "database"
const product = this.products.get(id) || null;
// Cache result
if (product) {
await this.cacheService.set(cacheKey, product, { ttl: 60 });
}
return product;
}
async findAll(): Promise<Product[]> {
const cacheKey = 'products:all';
const cached = await this.cacheService.get<Product[]>(cacheKey);
if (cached) {
return cached;
}
const products = Array.from(this.products.values());
await this.cacheService.set(cacheKey, products, { ttl: 30 });
return products;
}
async updateStock(id: string, quantity: number): Promise<Product | null> {
const product = this.products.get(id);
if (!product) return null;
product.stock += quantity;
this.products.set(id, product);
// Invalidate caches
await this.cacheService.deleteMany([
`product:${id}`,
'products:all',
]);
return product;
}
}
// Controller
@Controller('/products')
export class ProductController extends BaseController {
constructor(private productService: ProductService) {
super();
}
@Get('/')
async findAll(): Promise<Response> {
const products = await this.productService.findAll();
return this.success(products);
}
@Get('/:id')
async findOne(@Param('id') id: string): Promise<Response> {
const product = await this.productService.findById(id);
if (!product) {
return this.error('Product not found', 404, 404);
}
return this.success(product);
}
@Post('/:id/stock')
async updateStock(
@Param('id') id: string,
@Body() body: { quantity: number },
): Promise<Response> {
const product = await this.productService.updateStock(id, body.quantity);
if (!product) {
return this.error('Product not found', 404, 404);
}
return this.success(product);
}
}
// Module
@Module({
imports: [
CacheModule.forRoot({
type: CacheType.MEMORY,
cacheOptions: {
defaultTtl: 300000,
maxSize: 1000,
},
}),
],
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule {}