Skip to main content
This guide covers effective caching strategies using BentoCache in Sonamu.

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

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

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

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

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

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");
  }
}
TTL Formats:
  • '5s': 5 seconds
  • '5m': 5 minutes
  • '1h': 1 hour
  • '1d': 1 day
  • 0: Permanent (requires invalidation)

Parameter-based Caching

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

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

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

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

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

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

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

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

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

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

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

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)

// Simplest - auto-expire with TTL
@cache({ ttl: '5m' })  // Auto-expire after 5 minutes
async getData() {
  return this.getPuri();
}

2. Event-Based

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

// 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

// 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

// 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

// 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

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

100 requests:
- Average response time: 150ms
- DB queries: 100
- Total processing time: 15 seconds

After Caching

100 requests:
- Average response time: 5ms (cache hit)
- DB queries: 1 (first request)
- Total processing time: 0.5 seconds
Over 30x performance improvement!

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