What is BentoCache?
Sonamu provides powerful caching capabilities using BentoCache. Key Features:- Multiple drivers (memory, Redis, PostgreSQL, etc.)
- TTL (Time To Live) configuration
- Namespace-based management
- Cache Warming
- Tag-based invalidation
Cache Configuration
sonamu.config.ts Setup
Copy
import { bentostore } from "bentocache";
import { memoryDriver } from "bentocache/drivers/memory";
export default {
server: {
cache: {
default: "memory",
stores: {
memory: bentostore()
.useL1Layer(memoryDriver({
maxItems: 10_000,
maxSize: 100_000_000 // 100MB
}))
}
}
}
} satisfies SonamuConfig;
Redis Driver
Copy
import { bentostore } from "bentocache";
import { redisDriver } from "bentocache/drivers/redis";
export default {
server: {
cache: {
default: "redis",
stores: {
redis: bentostore()
.useL1Layer(redisDriver({
connection: {
host: process.env.REDIS_HOST || "localhost",
port: 6379,
password: process.env.REDIS_PASSWORD
}
}))
}
}
}
} satisfies SonamuConfig;
Multiple Drivers
Copy
export default {
server: {
cache: {
default: "memory",
stores: {
memory: bentostore()
.useL1Layer(memoryDriver({ maxSize: 50_000_000 })),
redis: bentostore()
.useL1Layer(redisDriver({
connection: { host: "localhost", port: 6379 }
}))
}
}
}
} satisfies SonamuConfig;
@cache Decorator
Basic Usage
Copy
import { cache, Puri } from "sonamu";
class UserModelClass extends BaseModel {
// Cache for 5 minutes
@cache({ ttl: '5m' })
async getActiveUsersCount(): Promise<number> {
const result = await this.getPuri()
.select({ count: Puri.count() })
.where({ status: "active" });
return result[0].count;
}
}
TTL Configuration
Copy
class ProductModelClass extends BaseModel {
// Cache for 1 hour
@cache({ ttl: '1h' })
async getFeaturedProducts() {
return this.getPuri()
.select("*")
.where({ featured: true });
}
// Cache for 1 day
@cache({ ttl: '1d' })
async getCategoryCounts() {
return this.getPuri()
.select("category", knex.raw("COUNT(*) as count"))
.groupBy("category");
}
// Permanent cache (requires manual invalidation)
@cache({ ttl: 0 })
async getProductCategories() {
return this.getPuri()
.select("DISTINCT category");
}
}
'5s': 5 seconds'5m': 5 minutes'1h': 1 hour'1d': 1 day0: Permanent (requires invalidation)
Parameter-based Caching
Copy
class UserModelClass extends BaseModel {
// Creates different cache entries per parameter
@cache({ ttl: '5m' })
async getUsersByStatus(status: string) {
return this.getPuri()
.select("id", "name", "email")
.where({ status });
}
}
// Each is stored with a different cache key
await UserModel.getUsersByStatus("active"); // UserModel.getUsersByStatus:active
await UserModel.getUsersByStatus("inactive"); // UserModel.getUsersByStatus:inactive
Custom Cache Keys
Copy
class PostModelClass extends BaseModel {
@cache({
ttl: '10m',
key: (userId: number, page: number) => `posts:user:${userId}:page:${page}`
})
async getUserPosts(userId: number, page: number = 1) {
return this.getPuri()
.select("*")
.where({ user_id: userId })
.offset((page - 1) * 20)
.limit(20);
}
}
Manual Cache Management
Using Sonamu.cache
Copy
import { Sonamu, Puri } from "sonamu";
class StatisticsService {
async getDailyStats(date: string) {
const cacheKey = `stats:daily:${date}`;
// Check cache
const cached = await Sonamu.cache.get(cacheKey);
if (cached) {
console.log("Cache hit!");
return cached;
}
// Cache miss - calculate
console.log("Cache miss, calculating...");
const stats = await this.calculateDailyStats(date);
// Store in cache (24 hours)
await Sonamu.cache.set(cacheKey, stats, { ttl: '24h' });
return stats;
}
private async calculateDailyStats(date: string) {
// Complex statistics calculation
const [userCount, orderCount, revenue] = await Promise.all([
UserModel.getPuri()
.select({ count: Puri.count() })
.then(r => r[0].count),
OrderModel.getPuri()
.select({ count: Puri.count() })
.where("created_at", ">=", date)
.then(r => r[0].count),
OrderModel.getPuri()
.select({ total: Puri.sum("total") })
.where("created_at", ">=", date)
.then(r => r[0].total)
]);
return {
date,
userCount,
orderCount,
revenue
};
}
}
Cache Invalidation
Copy
class UserModelClass extends BaseModel {
@api({ httpMethod: "POST" })
async createUser(params: UserSaveParams) {
const user = await this.save(params);
// Invalidate related caches
await Sonamu.cache.delete("users:active:count");
await Sonamu.cache.delete("users:stats");
return user;
}
@api({ httpMethod: "PUT" })
async updateUserStatus(id: number, status: string) {
await this.save({ id, status });
// Invalidate related caches with pattern matching
await Sonamu.cache.clear(); // Caution: deletes entire cache
}
}
getOrSet Pattern
Copy
class CacheService {
async getWithCache(key: string, factory: () => Promise<any>) {
return Sonamu.cache.getOrSet({
key,
ttl: '1h',
factory
});
}
async getUser(userId: number) {
return this.getWithCache(
`user:${userId}`,
() => UserModel.findById(userId)
);
}
}
Caching Strategies
1. Read-Through Pattern
Copy
class ProductModelClass extends BaseModel {
async getProductWithCache(id: number): Promise<Product> {
const cacheKey = `product:${id}`;
return Sonamu.cache.getOrSet({
key: cacheKey,
ttl: '1h',
factory: () => this.findById(id)
});
}
}
2. Write-Through Pattern
Copy
class ProductModelClass extends BaseModel {
async updateProduct(id: number, params: ProductSaveParams) {
// Update DB
const product = await this.save({ id, ...params });
// Update cache as well
await Sonamu.cache.set(`product:${id}`, product, { ttl: '1h' });
return product;
}
}
3. Cache-Aside Pattern
Copy
class ProductModelClass extends BaseModel {
async getProduct(id: number) {
// 1. Check cache
const cached = await Sonamu.cache.get(`product:${id}`);
if (cached) return cached;
// 2. Query DB
const product = await this.findById(id);
// 3. Store in cache (async after response)
setImmediate(async () => {
await Sonamu.cache.set(`product:${id}`, product, { ttl: '1h' });
});
return product;
}
}
4. Cache Warming
Copy
class ProductModelClass extends BaseModel {
// Warm popular products cache on server start
async warmPopularProducts() {
const popularProducts = await this.getPuri()
.select("*")
.where({ featured: true })
.orWhere("view_count", ">", 1000);
for (const product of popularProducts) {
await Sonamu.cache.set(
`product:${product.id}`,
product,
{ ttl: '1h' }
);
}
console.log(`Cache warming complete for ${popularProducts.length} products`);
}
}
Practical Examples
API Response Caching
Copy
class PostModelClass extends BaseModel {
// List API - 10 minute cache
@cache({ ttl: '10m' })
@api({ httpMethod: "GET" })
async listPosts(page: number = 1) {
return this.getPuri()
.select("id", "title", "author_id", "created_at")
.orderBy("created_at", "desc")
.offset((page - 1) * 20)
.limit(20);
}
// Detail API - 5 minute cache
@cache({ ttl: '5m' })
@api({ httpMethod: "GET" })
async getPost(id: number) {
const { qb } = this.getSubsetQueries("Detail");
const result = await this.executeSubsetQuery({
subset: "Detail",
qb: qb.where({ id }),
params: { num: 1, page: 1 }
});
return result.rows[0];
}
// Create API - invalidate related caches
@api({ httpMethod: "POST" })
async createPost(params: PostSaveParams) {
const post = await this.save(params);
// List cache invalidation is handled automatically (different keys)
// For specific cache invalidation, handle manually
return post;
}
}
Aggregation Data Caching
Copy
class DashboardService {
// Dashboard stats - 5 minute cache
async getDashboardStats() {
const cacheKey = "dashboard:stats";
return Sonamu.cache.getOrSet({
key: cacheKey,
ttl: '5m',
factory: async () => {
// Execute multiple aggregation queries in parallel
const [
totalUsers,
activeUsers,
totalOrders,
todayRevenue
] = await Promise.all([
UserModel.getPuri()
.select({ count: Puri.count() })
.then(r => r[0].count),
UserModel.getPuri()
.select({ count: Puri.count() })
.where({ status: "active" })
.then(r => r[0].count),
OrderModel.getPuri()
.select({ count: Puri.count() })
.then(r => r[0].count),
OrderModel.getPuri()
.select({ total: Puri.sum("total") })
.where("created_at", ">=", new Date().toISOString().split("T")[0])
.then(r => r[0].total)
]);
return {
totalUsers,
activeUsers,
totalOrders,
todayRevenue,
cachedAt: new Date()
};
}
});
}
}
User Session Caching
Copy
class AuthService {
// Store session (Redis recommended)
async saveSession(userId: number, token: string) {
const sessionData = {
userId,
token,
createdAt: new Date()
};
await Sonamu.cache.set(
`session:${token}`,
sessionData,
{ ttl: '24h' }
);
}
// Get session
async getSession(token: string) {
return Sonamu.cache.get(`session:${token}`);
}
// Logout
async logout(token: string) {
await Sonamu.cache.delete(`session:${token}`);
}
}
Cache Invalidation Strategies
1. Time-Based (TTL)
Copy
// Simplest - auto-expire with TTL
@cache({ ttl: '5m' }) // Auto-expire after 5 minutes
async getData() {
return this.getPuri();
}
2. Event-Based
Copy
class ProductModelClass extends BaseModel {
@api({ httpMethod: "POST" })
async createProduct(params: ProductSaveParams) {
const product = await this.save(params);
// Invalidate related caches
await this.invalidateProductCaches();
return product;
}
private async invalidateProductCaches() {
// Delete individually
await Sonamu.cache.delete("featured:products");
await Sonamu.cache.delete("stats:products");
}
}
Best Practices
1. Set Appropriate TTL
Copy
// Frequently changing data - short TTL
@cache({ ttl: '1m' }) // 1 minute
async getCurrentOnlineUsers() { }
// Rarely changing data - long TTL
@cache({ ttl: '1d' }) // 1 day
async getCountries() { }
// Static data - permanent
@cache({ ttl: 0 })
async getAppConfig() { }
2. Cache Key Naming
Copy
// Clear and hierarchical keys
"user:123"
"user:123:posts"
"stats:daily:2025-01-11"
"product:category:electronics"
// Unclear keys - avoid
"u123"
"data"
"temp"
3. Cache Size Management
Copy
// Cache only necessary fields for large objects
async cacheUser(user: User) {
const lightUser = {
id: user.id,
name: user.name,
email: user.email
// Exclude large fields
};
await Sonamu.cache.set(`user:${user.id}`, lightUser, { ttl: '1h' });
}
4. Cache Warming
Copy
// Pre-cache important data on server start
export default {
server: {
lifecycle: {
async onStart() {
await ProductModel.warmPopularProducts();
await CategoryModel.warmAllCategories();
console.log("Cache warming complete");
}
}
}
} satisfies SonamuConfig;
5. Failure Handling
Copy
async getDataWithFallback(id: number) {
try {
// Try cache
const cached = await Sonamu.cache.get(`data:${id}`);
if (cached) return cached;
} catch (error) {
console.error("Cache error:", error);
// Continue even if cache fails
}
// Query DB (when cache is missing or failed)
return this.findById(id);
}
Performance Comparison
Before Caching
Copy
100 requests:
- Average response time: 150ms
- DB queries: 100
- Total processing time: 15 seconds
After Caching
Copy
100 requests:
- Average response time: 5ms (cache hit)
- DB queries: 1 (first request)
- Total processing time: 0.5 seconds
Checklist
For effective caching:- Identify frequently queried data
- Set appropriate TTL (string format)
- Use clear cache key naming
- Invalidate cache on data changes
- Monitor memory usage
- Implement cache warming strategy
- Add failure handling logic