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}"
Auto-generated (Default)
Fixed Key
Function Generator
@ 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]"
}
@ cache ({ ttl: "1h" , key: "products:featured" })
async getFeatured () {
// Cache key: "products:featured"
// Always same key since no arguments
}
@ cache ({ ttl: "10m" , key: "user:profile" })
async getProfile ( userId : number ) {
// Cache key: "user:profile:123"
// Arguments automatically added as suffix
}
@ cache ({
ttl: "10m" ,
key : ( subset : string , id : number ) => `product: ${ subset } : ${ id } `
})
async findById ( subset : string , id : number ) {
// Cache key: "product:A:123"
}
@ cache ({
ttl: "1h" ,
key : ( filters : FilterParams ) => {
const sorted = Object . keys ( filters ). sort ();
return `search: ${ sorted . map ( k => ` ${ k } = ${ filters [ k ] } ` ). join ( ":" ) } ` ;
}
})
async search ( filters : FilterParams ) {
// Cache key: "search:category=electronics:price=100"
}
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
}
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
}
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
API Response Caching
User Profile
Aggregate Data
Search Results
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 ;
}
}
class UserModelClass extends BaseModelClass {
@ api ({ httpMethod: "GET" })
@ cache ({
ttl: "10m" ,
key : ( id : number ) => `user:profile: ${ id } ` ,
tags: [ "users" ]
})
async getProfile ( id : number ) {
const rdb = this . getPuri ( "r" );
const user = await rdb . table ( "users" )
. where ( "id" , id )
. first ();
const posts = await rdb . table ( "posts" )
. where ( "user_id" , id )
. limit ( 5 )
. select ();
return { user , posts };
}
@ api ({ httpMethod: "PUT" })
@ transactional ()
async updateProfile ( id : number , data : UserUpdateParams ) {
const wdb = this . getDB ( "w" );
await this . upsert ( wdb , { id , ... data });
// Invalidate only this user's cache
await Sonamu . cache . delete ( `user:profile: ${ id } ` );
return this . getProfile ( id );
}
}
class AnalyticsModelClass extends BaseModelClass {
@ api ({ httpMethod: "GET" })
@ cache ({ ttl: "1h" , key: "analytics:dashboard" })
async getDashboard () {
const rdb = this . getPuri ( "r" );
const [ users , orders , revenue ] = await Promise . all ([
rdb . table ( "users" ). count ( "* as count" ),
rdb . table ( "orders" ). count ( "* as count" ),
rdb . table ( "orders" ). sum ( "total_amount as total" )
]);
return {
totalUsers: users [ 0 ]. count ,
totalOrders: orders [ 0 ]. count ,
totalRevenue: revenue [ 0 ]. total
};
}
@ api ({ httpMethod: "GET" })
@ cache ({
ttl: "1d" ,
key : ( date : string ) => `analytics:daily: ${ date } `
})
async getDailyStats ( date : string ) {
const rdb = this . getPuri ( "r" );
return rdb . table ( "orders" )
. whereRaw ( "DATE(created_at) = ?" , [ date ])
. select (
rdb . raw ( "COUNT(*) as order_count" ),
rdb . raw ( "SUM(total_amount) as revenue" ),
rdb . raw ( "AVG(total_amount) as avg_order_value" )
)
. first ();
}
}
class SearchModelClass extends BaseModelClass {
@ api ({ httpMethod: "GET" })
@ cache ({
ttl: "15m" ,
key : ( query : string , filters : SearchFilters ) => {
const filterStr = Object . entries ( filters )
. sort (([ a ], [ b ]) => a . localeCompare ( b ))
. map (([ k , v ]) => ` ${ k } : ${ v } ` )
. join ( "," );
return `search: ${ query } : ${ filterStr } ` ;
},
tags: [ "search" ]
})
async search ( query : string , filters : SearchFilters ) {
const rdb = this . getPuri ( "r" );
let qb = rdb . table ( "products" )
. where ( "name" , "like" , `% ${ query } %` );
if ( filters . category ) {
qb = qb . where ( "category" , filters . category );
}
if ( filters . minPrice ) {
qb = qb . where ( "price" , ">=" , filters . minPrice );
}
if ( filters . maxPrice ) {
qb = qb . where ( "price" , "<=" , filters . maxPrice );
}
return qb . limit ( 20 ). select ();
}
@ api ({ httpMethod: "DELETE" })
async clearSearchCache () {
// Invalidate all search caches
await Sonamu . cache . deleteByTags ([ "search" ]);
return { success: true };
}
}
Next Steps