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

> Tag-based cache invalidation and management

To manage caches effectively, you need to invalidate them at appropriate times. Sonamu provides various invalidation methods.

## Why Invalidation is Needed

Caching improves performance, but can return **stale data**.

```typescript theme={null}
// 1. Get post (cached)
const post = await postModel.findById(1);
// { id: 1, title: "Original Title", views: 100 }

// 2. Update post
await postModel.update(1, { title: "Updated Title" });

// 3. Get again
const updatedPost = await postModel.findById(1);
// Still { id: 1, title: "Original Title", views: 100 } ❌
```

**Solution**: Invalidate related cache when data changes

## Invalidation Methods

### 1. Delete Specific Key

The most basic method.

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  @cache({ ttl: "10m" })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

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

    // Delete specific key
    await Sonamu.cache.delete({ key: `Post.findById:${id}` });

    return result;
  }
}
```

**Problem**: Must know the exact key and delete all related caches individually

### 2. Tag-based Invalidation (Recommended)

Using tags, you can **delete related caches as a group**.

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  // Assign tags
  @cache({ ttl: "10m", tags: ["post"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

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

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

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

    return result;
  }
}
```

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

### 3. Delete Multiple Keys

```typescript theme={null}
await Sonamu.cache.deleteMany({
  keys: ["user:1", "user:2", "user:3"]
});
```

### 4. Clear All Cache

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

**Caution**: Use carefully in production

## Tag Strategies

### Hierarchical Tag Design

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

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

  // Category list
  @cache({ ttl: "30m", tags: ["product", "product:list", "product:category"] })
  @api()
  async findByCategory(categoryId: number) {
    return this.findMany({ where: [['category_id', categoryId]] });
  }

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

### Selective Invalidation

```typescript theme={null}
class ProductModelClass extends BaseModelClass {
  @api()
  async create(data: ProductSave) {
    const result = await this.saveOne(data);

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

    return result;
  }

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

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

    return result;
  }

  @api()
  async updateViewCount(id: number) {
    const result = await this.updateOne(['id', id], {
      view_count: this.raw('view_count + 1')
    });

    // View count change only invalidates popular products cache
    await Sonamu.cache.deleteByTag({ tags: ["product:popular"] });

    return result;
  }

  @api()
  async deleteProduct(id: number) {
    const result = await this.deleteOne(['id', id]);

    // Invalidate all product-related cache
    await Sonamu.cache.deleteByTag({ tags: ["product"] });

    return result;
  }
}
```

### Dynamic Tags

Dynamically create tags that only affect specific entities.

```typescript theme={null}
class CommentModelClass extends BaseModelClass {
  @cache({
    key: (postId: number) => `comments:post:${postId}`,
    ttl: "10m",
    tags: (postId: number) => [`comment`, `comment:post:${postId}`]
  })
  @api()
  async findByPostId(postId: number) {
    return this.findMany({ where: [['post_id', postId]] });
  }

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

    // Invalidate only that post's comment cache
    await Sonamu.cache.deleteByTag({
      tags: [`comment:post:${data.post_id}`]
    });

    return result;
  }
}
```

**Note**: BentoCache's `@cache` decorator only supports static tags. For dynamic tags, you need to manipulate the cache directly:

```typescript theme={null}
@api()
async findByPostId(postId: number) {
  const cacheKey = `comments:post:${postId}`;
  const cacheTags = [`comment`, `comment:post:${postId}`];

  return Sonamu.cache.getOrSet({
    key: cacheKey,
    ttl: "10m",
    tags: cacheTags,
    factory: () => this.findMany({ where: [['post_id', postId]] })
  });
}
```

## Practical Examples

### 1. Post + Comment Invalidation

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  @cache({ ttl: "10m", tags: ["post"] })
  @api()
  async findById(id: number) {
    const post = await this.findOne(['id', id]);
    const comments = await commentModel.findByPostId(id);
    return { ...post, comments };
  }

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

class CommentModelClass extends BaseModelClass {
  @cache({ ttl: "10m", tags: ["comment"] })
  @api()
  async findByPostId(postId: number) {
    return this.findMany({ where: [['post_id', postId]] });
  }

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

    // On comment creation: Invalidate comment cache + post cache
    await Sonamu.cache.deleteByTag({ tags: ["comment", "post"] });

    return result;
  }
}
```

### 2. User + Permission Invalidation

```typescript theme={null}
class UserModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["user"] })
  @api()
  async findById(id: number) {
    const user = await this.findOne(['id', id]);
    const roles = await userRoleModel.findByUserId(id);
    return { ...user, roles };
  }
}

class UserRoleModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["user", "role"] })
  @api()
  async findByUserId(userId: number) {
    return this.findMany({ where: [['user_id', userId]] });
  }

  @api()
  async assign(userId: number, roleId: number) {
    const result = await this.saveOne({ user_id: userId, role_id: roleId });

    // Invalidate entire user cache (includes permission info)
    await Sonamu.cache.deleteByTag({ tags: ["user"] });

    return result;
  }
}
```

### 3. Category Hierarchy

```typescript theme={null}
class CategoryModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["category", "category:tree"] })
  @api()
  async getTree() {
    return this.buildTree(await this.findMany({}));
  }

  @cache({ ttl: "30m", tags: ["category", "category:products"] })
  @api()
  async getWithProducts(id: number) {
    const category = await this.findOne(['id', id]);
    const products = await productModel.findByCategory(id);
    return { ...category, products };
  }

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

    // Invalidate only category tree
    await Sonamu.cache.deleteByTag({ tags: ["category:tree"] });

    return result;
  }
}

class ProductModelClass extends BaseModelClass {
  @api()
  async updateCategory(productId: number, categoryId: number) {
    const result = await this.updateOne(['id', productId], { category_id: categoryId });

    // Invalidate category-product connection cache
    await Sonamu.cache.deleteByTag({ tags: ["category:products", "product"] });

    return result;
  }
}
```

### 4. Aggregate Data Invalidation

```typescript theme={null}
class OrderModelClass extends BaseModelClass {
  @cache({ ttl: "10m", tags: ["order", "order:stats"] })
  @api()
  async getStats() {
    const [total, pending, completed] = await Promise.all([
      this.count(),
      this.count({ where: [['status', 'pending']] }),
      this.count({ where: [['status', 'completed']] }),
    ]);
    return { total, pending, completed };
  }

  @api()
  async updateStatus(id: number, status: string) {
    const result = await this.updateOne(['id', id], { status });

    // Invalidate stats cache
    await Sonamu.cache.deleteByTag({ tags: ["order:stats"] });

    return result;
  }
}
```

## Isolation with Namespace

Using Namespace, you can isolate cache **per user, per tenant**.

### Per-User Cache

```typescript theme={null}
class UserDataModelClass extends BaseModelClass {
  @api()
  async getMyData(ctx: Context) {
    const userId = ctx.user.id;
    const userCache = Sonamu.cache.namespace(`user:${userId}`);

    return userCache.getOrSet({
      key: "my-data",
      ttl: "10m",
      factory: async () => {
        return this.findMany({ where: [['user_id', userId]] });
      }
    });
  }

  @api()
  async updateMyData(ctx: Context, data: any) {
    const userId = ctx.user.id;
    const result = await this.saveOne({ ...data, user_id: userId });

    // Invalidate only that user's cache
    const userCache = Sonamu.cache.namespace(`user:${userId}`);
    await userCache.clear();

    return result;
  }
}
```

### Multi-Tenant

```typescript theme={null}
class TenantDataModelClass extends BaseModelClass {
  @api()
  async getData(tenantId: number) {
    const tenantCache = Sonamu.cache.namespace(`tenant:${tenantId}`);

    return tenantCache.getOrSet({
      key: "data",
      ttl: "1h",
      tags: ["tenant-data"],
      factory: async () => {
        return this.findMany({ where: [['tenant_id', tenantId]] });
      }
    });
  }

  @api()
  async clearTenantCache(tenantId: number) {
    const tenantCache = Sonamu.cache.namespace(`tenant:${tenantId}`);

    // Delete all cache for specific tenant
    await tenantCache.clear();
  }
}
```

## Invalidation Strategies

### 1. Eager Invalidation (Immediate Invalidation)

Delete cache **immediately** when data changes.

```typescript theme={null}
@api()
async update(id: number, data: Partial<PostSave>) {
  const result = await this.updateOne(['id', id], data);

  // Immediate invalidation
  await Sonamu.cache.deleteByTag({ tags: ["post"] });

  return result;
}
```

**Advantages**:

* Simple and clear
* No stale data

**Disadvantages**:

* Reduced hit rate due to frequent cache deletion

### 2. Lazy Invalidation (Delayed Invalidation)

Rely on TTL for automatic expiration.

```typescript theme={null}
@cache({ ttl: "5m", tags: ["post"] })  // Auto-expires after 5 minutes
@api()
async findById(id: number) {
  return this.findOne(['id', id]);
}

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

  // No invalidation (auto-expires after 5 minutes)

  return result;
}
```

**Advantages**:

* High cache hit rate
* Simple code

**Disadvantages**:

* Potentially stale data for up to 5 minutes

### 3. Hybrid

Immediate invalidation for important changes, TTL for minor changes:

```typescript theme={null}
@api()
async update(id: number, data: Partial<PostSave>) {
  const result = await this.updateOne(['id', id], data);

  // Immediate invalidation only for title/content changes
  if (data.title || data.content) {
    await Sonamu.cache.deleteByTag({ tags: ["post"] });
  }
  // View count etc. rely on TTL

  return result;
}
```

### 4. Write-Through (Update on Write)

**Update** cache instead of deleting.

```typescript theme={null}
@api()
async update(id: number, data: Partial<PostSave>) {
  const result = await this.updateOne(['id', id], data);

  // Update cache
  const cacheKey = `Post.findById:${id}`;
  await Sonamu.cache.set({
    key: cacheKey,
    value: result,
    ttl: "10m",
    tags: ["post"]
  });

  return result;
}
```

**Advantages**:

* Cache always up-to-date
* High hit rate

**Disadvantages**:

* Complex cache key management
* Write performance degradation

## Invalidation in Distributed Environment

### Synchronization with Bus

When you have multiple servers, using **Bus** propagates invalidation to all servers.

```typescript theme={null}
// sonamu.config.ts
import Redis from "ioredis";

const redis = new Redis({ host: "localhost", port: 6379 });

export const config: SonamuConfig = {
  server: {
    cache: {
      default: "main",
      stores: {
        main: store()
          .useL1Layer(drivers.memory({ maxSize: "200mb" }))
          .useL2Layer(drivers.redis({ connection: redis }))
          .useBus(drivers.redisBus({ connection: redis })),  // Add Bus
      },
    },
  },
};
```

**How It Works**:

```
Server 1: deleteByTag("product")
  ↓
Bus: Publish "product tag invalidation" message
  ↓
Server 2, 3, 4: Receive Bus message → Delete L1 cache
```

### Using Without Bus

Without Bus, each server's L1 is independent:

```typescript theme={null}
// Configuration without Bus
store()
  .useL1Layer(drivers.memory({ maxSize: "200mb" }))
  .useL2Layer(drivers.redis({ connection: redis }))
// No Bus
```

**Problem**:

```
Server 1: deleteByTag("product") → L1/L2 deleted
Server 2: Still has stale cache in L1 ❌
```

**Solutions**:

1. Add Bus (recommended)
2. Use only L2 without L1 (slower)
3. Use short TTL (accept stale data)

## Monitoring

### Check Cache Key

```typescript theme={null}
// Check if specific key exists
const exists = await Sonamu.cache.has({ key: "post:1" });
console.log(exists);  // true or false

// Get value
const value = await Sonamu.cache.get({ key: "post:1" });
console.log(value);
```

### Check Before Tag Invalidation

You cannot check affected keys before invalidation, but consistent tag usage makes it predictable.

```typescript theme={null}
// Tag naming convention
const tags = {
  post: ["post"],                    // All posts
  postDetail: ["post", "detail"],    // Post detail
  postList: ["post", "list"],        // Post list
  comment: ["comment"],              // All comments
};

// Clear intent during invalidation
await Sonamu.cache.deleteByTag({ tags: ["post", "list"] });  // Lists only
await Sonamu.cache.deleteByTag({ tags: ["post"] });          // All posts
```

## Cautions

<Warning>
  **Cache Invalidation Cautions**:

  1. **Avoid Excessive Invalidation**: Too frequent invalidation reduces cache effectiveness
     ```typescript theme={null}
     // ❌ Bad example: Delete all cache on every view count increment
     async incrementViewCount(id: number) {
       await this.updateOne(['id', id], { views: this.raw('views + 1') });
       await Sonamu.cache.deleteByTag({ tags: ["post"] });  // Too excessive
     }

     // ✅ Good example: View count relies on TTL
     async incrementViewCount(id: number) {
       await this.updateOne(['id', id], { views: this.raw('views + 1') });
       // No invalidation, auto-expires via TTL
     }
     ```

  2. **Maintain Tag Consistency**: Consistent tag usage ensures accurate invalidation
     ```typescript theme={null}
     // ❌ Inconsistent
     @cache({ tags: ["Post"] })  // Uppercase
     @cache({ tags: ["post"] })  // Lowercase

     // ✅ Consistent
     @cache({ tags: ["post"] })
     @cache({ tags: ["post", "list"] })
     ```

  3. **Prevent Circular Invalidation**: If A invalidates B and B invalidates A, infinite loop occurs
     ```typescript theme={null}
     // ❌ Dangerous: Circular invalidation
     class AModel {
       @api()
       async update() {
         // ...
         await Sonamu.cache.deleteByTag({ tags: ["b"] });
       }
     }

     class BModel {
       @api()
       async update() {
         // ...
         await Sonamu.cache.deleteByTag({ tags: ["a"] });
       }
     }
     ```

  4. **Bus Required in Distributed Environment**: Operating multiple servers without Bus causes inconsistency
</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 Decorator" icon="at" href="/en/advanced-features/caching/cache-decorator">
    Apply caching to methods
  </Card>

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