> ## 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 Decorator

> Applying caching to methods

You can use the `@cache` decorator to automatically cache the results of Model or Frame methods.

## Basic Usage

### Simple Example

```typescript theme={null}
import { BaseModelClass, cache, api } from "sonamu";

class UserModelClass extends BaseModelClass {
  @cache({ ttl: "10m" })
  @api()
  async findById(id: number) {
    // This method is cached for 10 minutes
    return this.findOne(['id', id]);
  }
}
```

**How It Works**:

1. First call: DB query → Cache result
2. Second call (within 10 minutes): Return from cache (no DB query)
3. After 10 minutes: Query DB again → Refresh cache

## Decorator Options

### All Options

```typescript theme={null}
@cache({
  key?: string | ((...args: unknown[]) => string),  // Cache key
  store?: string,           // Store to use
  ttl?: string | number,    // Expiration time
  grace?: string | number,  // Grace period (Stale-While-Revalidate)
  tags?: string[],          // Tag list
  forceFresh?: boolean,     // Ignore cache and force refresh
})
```

### key: Cache Key Configuration

If no key is specified, it is **automatically generated**.

<Tabs>
  <Tab title="Auto-generated (Default)" icon="wand-magic-sparkles">
    ```typescript theme={null}
    class UserModelClass extends BaseModelClass {
      @cache({ ttl: "10m" })
      async findById(id: number) {
        return this.findOne(['id', id]);
      }
    }

    // Call: userModel.findById(123)
    // Auto-generated key: "User.findById:123"
    ```

    **Pattern**: `ModelName.methodName:serializedArgs`

    **Argument Serialization Rules**:

    * Single primitive (string/number/boolean): Used as-is
    * Complex objects: Uses JSON.stringify
    * No arguments: `ModelName.methodName` without suffix
  </Tab>

  <Tab title="String Key" icon="text">
    ```typescript theme={null}
    @cache({ key: "user-list", ttl: "5m" })
    async findAll() {
      return this.findMany({});
    }

    // Call: userModel.findAll()
    // Key: "user-list"

    @cache({ key: "user", ttl: "10m" })
    async findById(id: number) {
      return this.findOne(['id', id]);
    }

    // Call: userModel.findById(123)
    // Key: "user:123" (arguments auto-appended as suffix)
    ```

    **When arguments exist**: Auto-generated in `key:args` format
  </Tab>

  <Tab title="Function Key" icon="code">
    ```typescript theme={null}
    @cache({
      key: (userId: number, status: string) => `user:${userId}:${status}`,
      ttl: "10m"
    })
    async findByStatus(userId: number, status: string) {
      return this.findMany({ user_id: userId, status });
    }

    // Call: model.findByStatus(123, "active")
    // Key: "user:123:active"
    ```

    **Useful for complex key generation**
  </Tab>
</Tabs>

### ttl: Expiration Time

TTL (Time To Live) is the duration the cache remains valid.

```typescript theme={null}
// String format (recommended)
@cache({ ttl: "10s" })   // 10 seconds
@cache({ ttl: "5m" })    // 5 minutes
@cache({ ttl: "1h" })    // 1 hour
@cache({ ttl: "1d" })    // 1 day
@cache({ ttl: "1w" })    // 1 week
@cache({ ttl: "forever" }) // Permanent

// Number format (milliseconds)
@cache({ ttl: 60000 })  // 60000ms = 1 minute
```

**Without TTL**: BentoCache default value applies (typically unlimited)

### grace: Stale-While-Revalidate

Grace period is a feature that **returns stale cache** after TTL expiration while refreshing in the background.

```typescript theme={null}
@cache({
  ttl: "1m",      // Expires after 1 minute
  grace: "10m"    // Return stale value for 10 minutes after expiration
})
async getExpensiveData() {
  // Long-running operation
  return await this.heavyComputation();
}
```

**How It Works**:

1. **0\~1 minute**: Return fresh cache
2. **1\~11 minutes**: Immediately return stale cache + background refresh
3. **After 11 minutes**: Cache miss, recalculate

**Advantages**:

* Users always get fast response (even if stale, returns immediately)
* Refreshed in background so next user gets fresh data

### tags: Tag-based Invalidation

You can use tags to invalidate related caches as a group.

```typescript theme={null}
class ProductModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["product", "list"] })
  @api()
  async findAll() {
    return this.findMany({});
  }

  @cache({ ttl: "1h", tags: ["product"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

  @api()
  async update(id: number, data: Partial<Product>) {
    const result = await this.updateOne(['id', id], data);

    // Invalidate all caches with product tag
    await Sonamu.cache.deleteByTag({ tags: ["product"] });

    return result;
  }
}
```

**Invalidation Pattern**:

```typescript theme={null}
// When update() is called
await Sonamu.cache.deleteByTag({ tags: ["product"] });

// Result: Both findAll() and findById() caches are deleted
```

For more details, see [Cache Invalidation](/en/advanced-features/caching/cache-invalidation).

### store: Using a Specific Store

When multiple stores are configured, you can specify a particular store.

```typescript theme={null}
// Define multiple stores in sonamu.config.ts
export const config: SonamuConfig = {
  server: {
    cache: {
      default: "api",
      stores: {
        api: store().useL1Layer(drivers.memory({ maxSize: "200mb" })),
        database: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(drivers.redis({ connection: redis })),
      },
    },
  },
};

// Specify store in Model
class UserModelClass extends BaseModelClass {
  // Use api store (default)
  @cache({ ttl: "5m" })
  @api()
  async findAll() {
    return this.findMany({});
  }

  // Use database store (Redis shared)
  @cache({ store: "database", ttl: "1h" })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
}
```

### forceFresh: Ignore Cache

Use when you always want to fetch fresh data.

```typescript theme={null}
@cache({
  ttl: "1h",
  forceFresh: true  // Ignore cache and always run factory
})
async getRealTimeData() {
  return await this.fetchFromExternalAPI();
}
```

**Use Case**: Only for debugging or special cases (generally unnecessary)

## Practical Examples

### 1. API Response Caching

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  // Post list (5 minute cache)
  @cache({ ttl: "5m", tags: ["post", "list"] })
  @api()
  async findAll(page: number = 1) {
    return this.findMany({
      num: 20,
      page,
      order: [['created_at', 'desc']]
    });
  }

  // Post detail (10 minute cache)
  @cache({ ttl: "10m", tags: ["post"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

  // Popular posts (1 hour cache)
  @cache({
    key: "popular-posts",
    ttl: "1h",
    tags: ["post", "popular"]
  })
  @api()
  async findPopular() {
    return this.findMany({
      num: 10,
      where: [['view_count', '>', 1000]],
      order: [['view_count', 'desc']]
    });
  }
}
```

### 2. Cache Invalidation on Data Change

```typescript theme={null}
class ProductModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["product"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

  @cache({ ttl: "30m", tags: ["product", "list"] })
  @api()
  async findAll() {
    return this.findMany({});
  }

  @api()
  async create(data: ProductSave) {
    const result = await this.saveOne(data);

    // Invalidate only list tag (new item added to list)
    await Sonamu.cache.deleteByTag({ tags: ["list"] });

    return result;
  }

  @api()
  async update(id: number, data: Partial<ProductSave>) {
    const result = await this.updateOne(['id', id], data);

    // Invalidate all product tags (detail + list)
    await Sonamu.cache.deleteByTag({ tags: ["product"] });

    return result;
  }
}
```

### 3. Complex Key Generation

```typescript theme={null}
class OrderModelClass extends BaseModelClass {
  @cache({
    key: (userId: number, status: string, startDate: string) =>
      `order:user:${userId}:${status}:${startDate}`,
    ttl: "10m",
    tags: ["order"]
  })
  @api()
  async findByUserAndStatus(
    userId: number,
    status: string,
    startDate: string
  ) {
    return this.findMany({
      where: [
        ['user_id', userId],
        ['status', status],
        ['created_at', '>=', startDate]
      ]
    });
  }
}

// Call: model.findByUserAndStatus(123, "pending", "2025-01-01")
// Key: "order:user:123:pending:2025-01-01"
```

### 4. Using Stale-While-Revalidate

```typescript theme={null}
class AnalyticsModelClass extends BaseModelClass {
  @cache({
    ttl: "5m",      // Expires after 5 minutes
    grace: "1h",    // Use stale value for 1 hour
    tags: ["analytics"]
  })
  @api()
  async getDashboardStats() {
    // Heavy aggregation query
    const [users, orders, revenue] = await Promise.all([
      this.countUsers(),
      this.countOrders(),
      this.calculateRevenue(),
    ]);

    return { users, orders, revenue };
  }
}
```

**Scenario**:

* 0\~5 minutes: Fresh cache
* 5\~65 minutes: Immediately return stale cache + background recalculation
* After 65 minutes: Cache miss, recalculate

### 5. Permanent Caching for Configuration Values

```typescript theme={null}
class ConfigModelClass extends BaseModelClass {
  @cache({
    key: "app-config",
    ttl: "forever",  // Permanent caching
    tags: ["config"]
  })
  @api()
  async getAppConfig() {
    return this.findOne(['key', 'app_config']);
  }

  @api()
  async updateConfig(data: ConfigSave) {
    const result = await this.saveOne(data);

    // Invalidate cache on config change
    await Sonamu.cache.deleteByTag({ tags: ["config"] });

    return result;
  }
}
```

## Internal Method Calls and Cache Sharing

Cache is also shared when calling other methods within a Model.

```typescript theme={null}
class UserModelClass extends BaseModelClass {
  // Apply cache to findMany
  @cache({ ttl: "10m", tags: ["user"] })
  @api()
  async findMany(params: FindManyParams<User>) {
    return super.findMany(params);
  }

  // findById internally calls findMany
  async findById(id: number) {
    const { rows } = await this.findMany({
      where: [['id', id]],
      num: 1
    });
    return rows[0];
  }
}
```

**How It Works**:

```typescript theme={null}
// First call
await userModel.findById(123);
// → Calls findMany({ where: [['id', 123]], num: 1 })
// → DB query and caching

// Second call (same parameters)
await userModel.findById(123);
// → Calls findMany({ where: [['id', 123]], num: 1 })
// → Return from cache (no DB query)

// Direct findMany call also uses same cache
await userModel.findMany({ where: [['id', 123]], num: 1 });
// → Return from cache
```

## Cache Key Generation Logic

### Argument Serialization

```typescript theme={null}
// Inside decorator.ts
function serializeArgs(args: unknown[]): string {
  if (args.length === 0) return "";

  // Single primitive value
  if (args.length === 1) {
    const arg = args[0];
    if (arg === null || arg === undefined) return "";
    if (typeof arg === "string" || typeof arg === "number" || typeof arg === "boolean") {
      return String(arg);
    }
  }

  // Complex values are JSON serialized
  try {
    return JSON.stringify(args);
  } catch {
    // Use toString on serialization failure
    return args.map((arg) => String(arg)).join(":");
  }
}
```

**Examples**:

```typescript theme={null}
// Single primitive
serializeArgs([123]) → "123"
serializeArgs(["hello"]) → "hello"

// Complex object
serializeArgs([{ id: 1, name: "test" }]) → '[{"id":1,"name":"test"}]'

// Multiple arguments
serializeArgs([123, "active"]) → '[123,"active"]'
```

### Full Key Generation

```typescript theme={null}
function generateCacheKey(
  modelName: string,
  methodName: string,
  args: unknown[],
  keyOption?: string | Function,
): string {
  // 1. Custom key function
  if (typeof keyOption === "function") {
    return keyOption(...args);
  }

  // 2. String key + args suffix
  if (typeof keyOption === "string") {
    const argsSuffix = serializeArgs(args);
    return argsSuffix ? `${keyOption}:${argsSuffix}` : keyOption;
  }

  // 3. Auto-generated
  const baseKey = `${modelName}.${methodName}`;
  const argsSuffix = serializeArgs(args);
  return argsSuffix ? `${baseKey}:${argsSuffix}` : baseKey;
}
```

## Direct Cache Manipulation

You can also manipulate the cache directly without decorators.

```typescript theme={null}
import { Sonamu } from "sonamu";

class UserModelClass extends BaseModelClass {
  @api()
  async findById(id: number) {
    const cacheKey = `user:${id}`;

    // Check cache
    const cached = await Sonamu.cache.get({ key: cacheKey });
    if (cached) {
      return cached;
    }

    // DB query
    const user = await this.findOne(['id', id]);

    // Cache
    await Sonamu.cache.set({
      key: cacheKey,
      value: user,
      ttl: "10m",
      tags: ["user"]
    });

    return user;
  }
}
```

**Decorator vs Direct Manipulation**:

* **Decorator**: Concise, declarative, automatic key generation
* **Direct Manipulation**: Complex logic, conditional caching, fine-grained control

## Cautions

<Warning>
  **@cache Decorator Cautions**:

  1. **Cache Manager Initialization Required**: Error if no cache configuration in `sonamu.config.ts`
     ```typescript theme={null}
     // Error: CacheManager is not initialized
     @cache({ ttl: "10m" })
     async findById(id: number) { ... }
     ```

  2. **Async Methods Only**: Cannot be used with synchronous methods
     ```typescript theme={null}
     // ❌ Error
     @cache({ ttl: "10m" })
     findByIdSync(id: number) { ... }

     // ✅ OK
     @cache({ ttl: "10m" })
     async findById(id: number) { ... }
     ```

  3. **Store Name Matching**: The `store` option must match names defined in configuration
     ```typescript theme={null}
     // sonamu.config.ts
     stores: {
       myStore: store()...
     }

     // ❌ Error
     @cache({ store: "wrongName", ttl: "10m" })

     // ✅ OK
     @cache({ store: "myStore", ttl: "10m" })
     ```

  4. **Serializable Values Only**: Functions, Symbols, etc. cannot be cached
     ```typescript theme={null}
     // ❌ Bad example
     @cache({ ttl: "10m" })
     async getProcessor() {
       return {
         process: () => { ... }  // Functions cannot be serialized
       };
     }

     // ✅ Good example
     @cache({ ttl: "10m" })
     async getData() {
       return {
         id: 1,
         name: "test",
         values: [1, 2, 3]
       };
     }
     ```

  5. **Argument Order Matters**: Same values in different order result in different keys
     ```typescript theme={null}
     @cache({ ttl: "10m" })
     async find(name: string, age: number) { ... }

     find("John", 30)  // Key: "Model.find:["John",30]"
     find(30, "John")  // Key: "Model.find:[30,"John"]" (different key!)
     ```
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Cache Configuration" icon="gear" href="/en/advanced-features/caching/cache-configuration">
    Configure Stores and Drivers
  </Card>

  <Card title="Cache Invalidation" icon="trash" href="/en/advanced-features/caching/cache-invalidation">
    Tag-based cache invalidation
  </Card>

  <Card title="Cache Strategies" icon="chess" href="/en/advanced-features/caching/cache-strategies">
    TTL, Grace, Namespace usage
  </Card>
</CardGroup>
