Skip to main content
Sonamu provides powerful caching capabilities based on BentoCache. This guide explains how to configure caching in sonamu.config.ts.

What is BentoCache?

BentoCache is a TypeScript cache library that supports multi-tier caching. Key Features:
  • L1/L2 Layers: Memory (fast) + Persistent storage (slower but shareable)
  • Various Drivers: Memory, Redis, File, Knex support
  • Bus System: Distributed cache invalidation
  • Tag-based Invalidation: Manage multiple caches as groups
  • TTL & Grace Period: Expiration and Stale-While-Revalidate

Basic Configuration

sonamu.config.ts

Add cache configuration to the server.cache field in sonamu.config.ts:
import { drivers, store, type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  // ... other settings
  server: {
    cache: {
      default: "main",
      stores: {
        main: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
      },
    },
  },
};
Required Fields:
  • default: Name of the default store to use
  • stores: Store configuration object (at least 1 required)

Driver Types

Sonamu provides 5 types of drivers:

memory

Memory-based cache (Fast but cleared on process restart)

redis

Redis-based cache (Shareable across multiple processes)

file

File system-based cache (Persistent storage)

knex

Database-based cache (Uses existing DB)

redisBus

Distributed cache invalidation bus (Synchronization across multiple servers)

Import Methods

// 1. Using drivers object (recommended)
import { drivers } from "sonamu/cache";
drivers.memory({ maxSize: "100mb" });
drivers.redis({ connection: {...} });

// 2. Individual imports
import { memoryDriver, redisDriver } from "sonamu/cache";
memoryDriver({ maxSize: "100mb" });
redisDriver({ connection: {...} });

Store Configuration

L1 Layer (Memory Cache)

L1 is stored in local memory and is the fastest.
store().useL1Layer(drivers.memory({
  maxSize: "100mb",  // Maximum size
  maxItems: 1000,    // Maximum number of items
}))
Characteristics:
  • Valid only within the process
  • Cleared on server restart
  • No network I/O (fastest)

L2 Layer (Persistent Storage)

L2 is shareable storage across multiple processes/servers.
import { drivers, store } from "sonamu/cache";
import Redis from "ioredis";

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

store()
  .useL1Layer(drivers.memory({ maxSize: "100mb" }))
  .useL2Layer(drivers.redis({ connection: redis }))
Redis Advantages:
  • Share cache across multiple servers
  • Persistent storage (retained after restart)
  • Fast network access

Bus Layer (Distributed Invalidation)

When you have multiple servers, deleting cache on one server notifies the others.
import Redis from "ioredis";

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

store()
  .useL1Layer(drivers.memory({ maxSize: "100mb" }))
  .useL2Layer(drivers.redis({ connection: redis }))
  .useBus(drivers.redisBus({ connection: redis }))
Without Bus:
Server 1: deleteByTag("product") β†’ L1/L2 deleted
Server 2: Still using stale cache ❌
With Bus:
Server 1: deleteByTag("product") β†’ L1/L2 deleted + Bus notification
Server 2: Receives Bus message β†’ L1 deleted βœ…

Multi-Store Configuration

You can use multiple stores for different purposes:
export const config: SonamuConfig = {
  server: {
    cache: {
      default: "api",  // Default store
      stores: {
        // API response caching (short TTL, memory only)
        api: store()
          .useL1Layer(drivers.memory({ maxSize: "200mb" })),

        // Database query caching (long TTL, Redis shared)
        database: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(drivers.redis({ connection: redis }))
          .useBus(drivers.redisBus({ connection: redis })),

        // Static config caching (permanent, file storage)
        config: store()
          .useL1Layer(drivers.memory({ maxItems: 100 }))
          .useL2Layer(drivers.file({ directory: ".config-cache" })),
      },
    },
  },
};

Using Stores

// Use default store
await Sonamu.cache.set({ key: "user:1", value: {...} });

// Use specific store
await Sonamu.cache.use("database").set({ key: "query:1", value: {...} });
await Sonamu.cache.use("config").set({ key: "settings", value: {...} });
You can also specify stores in decorators:
class UserModelClass extends BaseModel {
  // Use database store
  @cache({ store: "database", ttl: "1h" })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }

  // Use config store
  @cache({ store: "config", ttl: "forever" })
  async getSettings() {
    return this.findOne(['key', 'settings']);
  }
}

Practical Examples

1. Single Server (Memory Only)

export const config: SonamuConfig = {
  server: {
    cache: {
      default: "main",
      stores: {
        main: store().useL1Layer(
          drivers.memory({
            maxSize: "500mb",
            maxItems: 10000,
          })
        ),
      },
    },
  },
};
Suitable Cases:
  • Single server operation
  • Fast cache regeneration
  • Infrequent server restarts

2. Multiple Servers (Redis Shared)

import Redis from "ioredis";

const redis = new Redis({
  host: process.env.REDIS_HOST ?? "localhost",
  port: Number(process.env.REDIS_PORT) ?? 6379,
  password: process.env.REDIS_PASSWORD,
});

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 })),
      },
    },
  },
};
Suitable Cases:
  • Load balancer + multiple servers
  • Need to share cache across servers
  • Maintain cache after server restart

3. Tier-Based Configuration (Performance Optimization)

import Redis from "ioredis";

const redis = new Redis({ /* ... */ });

export const config: SonamuConfig = {
  server: {
    cache: {
      default: "main",
      stores: {
        // Fast lookup (API responses)
        main: store()
          .useL1Layer(drivers.memory({ maxSize: "300mb" })),

        // Sharing required (user sessions)
        shared: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(drivers.redis({ connection: redis }))
          .useBus(drivers.redisBus({ connection: redis })),

        // Permanent storage (settings, templates)
        persistent: store()
          .useL1Layer(drivers.memory({ maxItems: 100 }))
          .useL2Layer(drivers.file({ directory: ".persistent-cache" })),
      },
    },
  },
};

Driver Options in Detail

Memory Driver

drivers.memory({
  maxSize: "100mb",     // Maximum size (string or bytes)
  maxItems: 1000,       // Maximum number of items
  maxItemSize: "10mb",  // Maximum size per item
})
Size Units: "10kb", "5mb", "1gb" or bytes (1024)

Redis Driver

import Redis from "ioredis";

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

drivers.redis({
  connection: redis,
  keyPrefix: "cache:",  // Key prefix (optional)
})

File Driver

drivers.file({
  directory: ".cache",  // Storage directory
  prefix: "sonamu",     // Filename prefix (optional)
})

Knex Driver

import { DB } from "sonamu";

drivers.knex({
  connection: DB.getDB("w"),
  tableName: "cache_items",  // Table name (default)
})
Table Schema (auto-created):
CREATE TABLE cache_items (
  key TEXT PRIMARY KEY,
  value TEXT,
  expires_at INTEGER
);

Redis Bus Driver

drivers.redisBus({
  connection: redis,
  channelPrefix: "cache:",  // Channel prefix (optional)
})

Test Environment

In test environments, the memory driver is automatically used:
// No separate configuration needed for tests
import { Sonamu } from "sonamu";

test("cache test", async () => {
  // Automatically uses memory cache
  await Sonamu.cache.set({ key: "test", value: "value" });

  const result = await Sonamu.cache.get({ key: "test" });
  expect(result).toBe("value");
});
Internal Implementation:
// sonamu/src/api/sonamu.ts
private async initializeCache(config: CacheConfig | undefined, forTesting: boolean) {
  if (forTesting) {
    const { createTestCacheManager } = await import("../cache/cache-manager");
    this._cache = createTestCacheManager();  // Memory driver
    return;
  }
  // ...
}

Cautions

Cache Configuration Cautions:
  1. Memory Limits: Setting maxSize too large can cause OOM (Out of Memory)
    // ❌ Bad example
    drivers.memory({ maxSize: "10gb" })  // Larger than server memory
    
    // βœ… Good example
    drivers.memory({ maxSize: "500mb" })  // Appropriate size
    
  2. Redis Connection Sharing: Use the same instance for driver and bus
    // βœ… Good example
    const redis = new Redis({...});
    store()
      .useL2Layer(drivers.redis({ connection: redis }))
      .useBus(drivers.redisBus({ connection: redis }))
    
  3. Store Name Matching: The store option in decorators must match names defined in configuration
    // sonamu.config.ts
    stores: {
      myStore: store()...
    }
    
    // ❌ Bad example
    @cache({ store: "wrongName" })  // Error
    
    // βœ… Good example
    @cache({ store: "myStore" })
    
  4. Using L2 Without Bus: If multiple servers use only L2 (Redis), L1 won’t be synchronized
    // ❌ Risky: L1 inconsistency possible between servers
    store()
      .useL1Layer(drivers.memory({ maxSize: "100mb" }))
      .useL2Layer(drivers.redis({ connection: redis }))
    // No Bus β†’ L1 on other servers remains unchanged during invalidation
    
    // βœ… Safe: L1 synchronized via Bus
    store()
      .useL1Layer(drivers.memory({ maxSize: "100mb" }))
      .useL2Layer(drivers.redis({ connection: redis }))
      .useBus(drivers.redisBus({ connection: redis }))
    

Next Steps