> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @cache

> Method Result Caching Decorator

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

```typescript theme={null}
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.

```typescript theme={null}
import { defineConfig } from "sonamu";
import { drivers, store } from "sonamu/cache";

export default defineConfig({
  server: {
    cache: {
      default: "multi", // Default store
      stores: {
        // Memory only
        memory: store().useL1Layer(drivers.memory({ maxSize: "100mb" })),

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

        // Multi-tier (Memory + Redis)
        multi: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(
            drivers.redis({
              connection: { host: "127.0.0.1", port: 6379 },
            }),
          ),
      },
    },
  },
});
```

<Info>Without cache configuration, using `@cache` will cause an error.</Info>

## Options

### ttl

Specifies cache validity time.

```typescript theme={null}
type TTL = string | number; // "10s", "5m", "1h", "1d" or milliseconds
```

```typescript theme={null}
@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() {}
```

<Tip>String format is more readable: `"10s"`, `"5m"`, `"1h"`, `"1d"`</Tip>

### key

Specifies the cache key.

**Default:** `"{ModelName}.{methodName}:{serializedArgs}"`

<Tabs>
  <Tab title="Auto-generated (Default)" icon="wand-magic-sparkles">
    ```typescript theme={null}
    @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]"
    }
    ```
  </Tab>

  <Tab title="Fixed Key" icon="key">
    ```typescript theme={null}
    @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
    }
    ```
  </Tab>

  <Tab title="Function Generator" icon="code">
    ```typescript theme={null}
    @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"
    }
    ```
  </Tab>
</Tabs>

### store

Specifies the cache store to use.

**Default:** `default` store from `sonamu.config.ts`

```typescript theme={null}
@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.

```typescript theme={null}
@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

<Info>
  Advanced options like `grace` and `timeouts` are provided by BentoCache's `RawCommonOptions`. See
  [BentoCache documentation](https://bentocache.dev) for details.
</Info>

Grace period that returns the previous value (Stale) for a certain time after cache expires (Stale-While-Revalidate pattern).

```typescript theme={null}
@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

<Info>
  Stale-While-Revalidate provides fast responses to users (even if stale), while refreshing in the
  background so the next user gets fresh data.
</Info>

### timeouts

Sets timeouts for cache operations.

```typescript theme={null}
@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

```typescript theme={null}
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

```typescript theme={null}
// Delete specific key
await Sonamu.cache.delete("Product.findById:123");

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

### Clear All

```typescript theme={null}
// Delete all caches
await Sonamu.cache.clear();
```

## Using with Other Decorators

### With @api

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

### With @transactional

```typescript theme={null}
@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 };
}
```

<Warning>Decorator order: `@api` → `@cache` → `@transactional`</Warning>

## How Cache Works

### 1. Cache Hit

```typescript theme={null}
@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

```typescript theme={null}
@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

```typescript theme={null}
@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

```typescript theme={null}
// ❌ 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:

```typescript theme={null}
@cache({ ttl: "10m" })
async search(params: { name: string; age: number }) {
  // Cache key: "User.search:{"name":"Alice","age":30}"
}
```

<Tip>
  For objects that are difficult to serialize, use the `key` function to generate keys directly.
</Tip>

### 3. Caching null/undefined

`null` and `undefined` are also cached:

```typescript theme={null}
@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**:

```typescript theme={null}
// ❌ 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:

```typescript theme={null}
// 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:

```typescript theme={null}
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:

```typescript theme={null}
export default defineConfig({
  cache: {
    stores: {
      memory: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))
        .options({
          logger: {
            log: (level, message) => {
              console.log(`[${level}] ${message}`);
            },
          },
        }),
    },
  },
});
```

## Examples

<Tabs>
  <Tab title="API Response Caching" icon="bolt">
    ```typescript theme={null}
    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;
      }
    }
    ```
  </Tab>

  <Tab title="User Profile" icon="user">
    ```typescript theme={null}
    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);
      }
    }
    ```
  </Tab>

  <Tab title="Aggregate Data" icon="chart-line">
    ```typescript theme={null}
    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();
      }
    }
    ```
  </Tab>

  <Tab title="Search Results" icon="magnifying-glass">
    ```typescript theme={null}
    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 };
      }
    }
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="@api" icon="plug" href="/en/api-reference/decorators/api">
    Create API endpoints
  </Card>

  <Card title="@transactional" icon="arrows-rotate" href="/en/api-reference/decorators/transactional">
    Use transactions
  </Card>

  <Card title="BentoCache" icon="book" href="https://bentocache.dev">
    BentoCache official documentation
  </Card>

  <Card title="Performance Optimization" icon="gauge-high" href="/en/guides/performance">
    Performance optimization guide
  </Card>
</CardGroup>
