Skip to main content
The @cache decorator caches method execution results to improve performance. Based on BentoCache, it supports various storage options including memory, Redis, and DynamoDB.

Basic Usage

import { BaseModelClass, api, cache } from "sonamu";

class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  @cache({ ttl: "10m" })
  async findById(id: number) {
    const rdb = this.getPuri("r");
    return rdb.table("products").where("id", id).first();
  }

  @api({ httpMethod: "GET" })
  @cache({ ttl: "1h", tags: ["products"] })
  async list() {
    const rdb = this.getPuri("r");
    return rdb.table("products").select();
  }
}

Configuration (sonamu.config.ts)

Cache configuration is required in sonamu.config.ts to use caching.
import { bentostore } from "bentocache";
import { memoryDriver } from "bentocache/drivers/memory";
import { redisDriver } from "bentocache/drivers/redis";
import { defineConfig } from "sonamu";

export default defineConfig({
  cache: {
    default: "multi",  // Default store
    stores: {
      // Memory only
      memory: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 })),

      // Redis only
      redis: bentostore()
        .useL1Layer(redisDriver({
          connection: { host: "127.0.0.1", port: 6379 }
        })),

      // Multi-tier (Memory + Redis)
      multi: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))
        .useL2Layer(redisDriver({
          connection: { host: "127.0.0.1", port: 6379 }
        }))
    }
  }
});
Without cache configuration, using @cache will cause an error.

Options

ttl

Specifies cache validity time.
type TTL = string | number;  // "10s", "5m", "1h", "1d" or milliseconds
@cache({ ttl: "10m" })     // 10 minutes
async getData() {}

@cache({ ttl: "1h" })      // 1 hour
async getReport() {}

@cache({ ttl: "1d" })      // 1 day
async getDailySummary() {}

@cache({ ttl: 60000 })     // 60 seconds (milliseconds)
async getQuickData() {}
String format is more readable: "10s", "5m", "1h", "1d"

key

Specifies the cache key. Default: "{ModelName}.{methodName}:{serializedArgs}"
@cache({ ttl: "10m" })
async findById(id: number) {
  // Cache key: "Product.findById:123"
}

@cache({ ttl: "10m" })
async search(query: string, page: number) {
  // Cache key: "Product.search:["search term",1]"
}

store

Specifies the cache store to use. Default: default store from sonamu.config.ts
@cache({ ttl: "1h", store: "redis" })
async getSharedData() {
  // Uses Redis store
}

@cache({ ttl: "5m", store: "memory" })
async getLocalData() {
  // Uses memory store
}

tags

Specifies cache tags. Used for bulk cache invalidation by tag.
@cache({ ttl: "1h", tags: ["products", "featured"] })
async getFeatured() {
  // Managed with tags
}

@cache({ ttl: "10m", tags: ["products"] })
async findById(id: number) {
  // Can be invalidated with "products" tag
}

// Invalidate cache
await Sonamu.cache.deleteByTags(["products"]);

grace

Advanced options like grace and timeouts are provided by BentoCache’s RawCommonOptions. See BentoCache documentation for details.
Grace period that returns the previous value (Stale) for a certain time after cache expires (Stale-While-Revalidate pattern).
@cache({
  ttl: "10m",
  grace: "5m"  // Return stale value for 5 minutes after TTL expires
})
async getData() {
  // 0-10 min: Return fresh cache
  // 10-15 min: Return stale cache immediately + background refresh
  // After 15 min: Cache miss, recalculate
}
How it works:
  • Within TTL: Return fresh cache
  • Within grace period after TTL: Return stale cache immediately, refresh in background
  • After grace period: Cache miss
Stale-While-Revalidate provides fast responses to users (even if stale), while refreshing in the background so the next user gets fresh data.

timeouts

Sets timeouts for cache operations.
@cache({
  ttl: "10m",
  timeouts: {
    soft: "100ms",   // Execute factory if no cache within this time
    hard: "1s"       // Maximum wait time
  }
})
async getData() {
  // If no cache within 100ms, execute factory immediately
}

Cache Invalidation

Invalidate by Tag

class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  @cache({ ttl: "1h", tags: ["products"] })
  async list() {
    const rdb = this.getPuri("r");
    return rdb.table("products").select();
  }

  @api({ httpMethod: "POST" })
  @transactional()
  async create(data: ProductCreateParams) {
    const wdb = this.getDB("w");
    const result = await this.insert(wdb, data);

    // Invalidate all caches with "products" tag
    await Sonamu.cache.deleteByTags(["products"]);

    return result;
  }
}

Invalidate by Key

// Delete specific key
await Sonamu.cache.delete("Product.findById:123");

// Delete with pattern matching
await Sonamu.cache.deleteMany("Product.findById:*");

Clear All

// Delete all caches
await Sonamu.cache.clear();

Using with Other Decorators

With @api

@api({ httpMethod: "GET" })
@cache({ ttl: "10m" })
async getData() {
  // API endpoint + caching
}

With @transactional

@api({ httpMethod: "GET" })
@cache({ ttl: "10m" })
@transactional({ readOnly: true })
async getComplexData() {
  // Read-only transaction + caching
  const rdb = this.getPuri("r");

  const users = await rdb.table("users").select();
  const posts = await rdb.table("posts").select();

  return { users, posts };
}
Decorator order: @api@cache@transactional

How Cache Works

1. Cache Hit

@cache({ ttl: "10m" })
async findById(id: number) {
  // First call: Query DB + save to cache
  // Second call: Return from cache immediately (no DB query)
}

2. Cache Miss

@cache({ ttl: "10m" })
async findById(id: number) {
  // Not in cache → execute factory (run method)
  const result = await this.queryDB(id);
  // Save result to cache
  return result;
}

3. Cache Refresh

@cache({ ttl: "10m" })
async getData() {
  // First call after TTL expires: Execute factory + cache new value
  // Next 10 minutes: Return cached value
}

Precautions

1. CacheManager Initialization Required

// ❌ Error
@cache({ ttl: "10m" })
async getData() {}
// CacheManager is not initialized
Solution: Add cache configuration in sonamu.config.ts

2. Argument Serialization

Complex objects are automatically JSON serialized:
@cache({ ttl: "10m" })
async search(params: { name: string; age: number }) {
  // Cache key: "User.search:{"name":"Alice","age":30}"
}
For objects that are difficult to serialize, use the key function to generate keys directly.

3. Caching null/undefined

null and undefined are also cached:
@cache({ ttl: "10m" })
async findById(id: number) {
  const result = await this.queryDB(id);
  // Result is cached even if null
  return result;  // null
}

4. Methods with Side Effects

Use cache only for pure functions:
// ❌ Bad: Has side effects
@cache({ ttl: "10m" })
async incrementCounter() {
  this.counter++;  // Side effect
  return this.counter;
}

// ✅ Good: Pure function
@cache({ ttl: "10m" })
async getCounter() {
  return this.counter;  // Read only
}

Performance Optimization

Multi-tier Caching

Fast memory cache + persistent Redis cache:
// sonamu.config.ts
export default defineConfig({
  cache: {
    stores: {
      multi: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))  // Fast
        .useL2Layer(redisDriver({ /* ... */ }))        // Persistent
    }
  }
});

// If in L1 (memory), return immediately
// If not in L1, query L2 (Redis)
// If not in L2, execute factory
@cache({ ttl: "1h", store: "multi" })
async getData() {}

Cache Warmup

Pre-populate cache:
class ProductModelClass extends BaseModelClass {
  async warmupCache() {
    // Pre-cache popular products
    const popularIds = [1, 2, 3, 4, 5];
    await Promise.all(
      popularIds.map(id => this.findById(id))
    );
  }

  @cache({ ttl: "1h" })
  async findById(id: number) {
    const rdb = this.getPuri("r");
    return rdb.table("products").where("id", id).first();
  }
}

Logging

To log cache hits/misses, use BentoCache configuration:
export default defineConfig({
  cache: {
    stores: {
      memory: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))
        .options({
          logger: {
            log: (level, message) => {
              console.log(`[${level}] ${message}`);
            }
          }
        })
    }
  }
});

Examples

class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  @cache({ ttl: "5m", tags: ["products"] })
  async list(params: ProductListParams) {
    const rdb = this.getPuri("r");
    return rdb.table("products")
      .where("active", true)
      .paginate(params);
  }

  @api({ httpMethod: "GET" })
  @cache({ ttl: "1h", tags: ["products"] })
  async getFeatured() {
    const rdb = this.getPuri("r");
    return rdb.table("products")
      .where("featured", true)
      .limit(10);
  }

  @api({ httpMethod: "POST" })
  @transactional()
  async create(data: ProductCreateParams) {
    const wdb = this.getDB("w");
    const result = await this.insert(wdb, data);

    // Invalidate cache
    await Sonamu.cache.deleteByTags(["products"]);

    return result;
  }
}

Next Steps