Skip to main content

Caching

CacheService provides a small driver-based cache API for application code and first-party packages. The core package includes an in-memory driver by default and a Redis driver for distributed deployments.

Use caching when a value is expensive to compute or fetch and can be reused safely for a period of time.

Common examples:

  • Configuration loaded from a database.
  • Public product catalogs.
  • Expensive aggregate queries.
  • Feature flag snapshots.
  • External API responses.

Avoid caching values that must reflect every write immediately unless you have a clear invalidation strategy.

Default Cache Service

Carno registers a CacheService automatically during application bootstrap.

import { CacheService, Service } from '@carno.js/core';

@Service()
export class ProductService {
constructor(private cache: CacheService) {}
}

By default, CacheService uses MemoryDriver.

Basic Operations

await this.cache.set('product:1', { id: 1, name: 'Phone' }, 60000);

const product = await this.cache.get<{ id: number; name: string }>('product:1');

if (await this.cache.has('product:1')) {
await this.cache.del('product:1');
}

Methods:

MethodDescription
get<T>(key)Returns a cached value or null
set<T>(key, value, ttl?)Stores a value
del(key)Deletes one key
has(key)Checks whether a non-expired key exists
clear()Clears the driver
getMany(keys)Reads many keys
setMany(entries)Stores many keys
delMany(keys)Deletes many keys
getOrSet(key, cb, ttl?)Cache-aside helper
close()Closes the driver if it has resources

TTL Units

TTL values are numbers in milliseconds.

await this.cache.set('profile:42', profile, 30_000); // 30 seconds
await this.cache.set('settings', settings, 3_600_000); // 1 hour

If no TTL is provided, the value uses the service default TTL. If no default TTL exists, the value does not expire automatically.

Cache-Aside With getOrSet

getOrSet() implements the common cache-aside pattern:

  1. Try to read from cache.
  2. If present, return the cached value.
  3. If missing, compute it.
  4. Store it.
  5. Return it.
async getProduct(id: string) {
return this.cache.getOrSet(
`product:${id}`,
async () => {
return this.products.findById(id);
},
30_000,
);
}

This keeps controller and service code from repeating the same check-then-fetch boilerplate.

Singleflight (same process only)

When several callers miss the same key at the same time, getOrSet runs the callback once and shares the result with every concurrent waiter on that CacheService instance.

  • Coordination is local to the process (an in-memory map of in-flight promises).
  • Multi-instance deployments with Redis still run one computation per process on a shared miss; there is no distributed lock.
  • If the callback rejects, all concurrent waiters see the error and a later call may retry.

Key Prefixing

Use a prefix to isolate environments, modules or tenants.

import { CacheService } from '@carno.js/core';

const cache = new CacheService({
prefix: 'my-app',
});

await cache.set('settings', settings);
// Stored as my-app:settings in the driver.

Prefixing is especially useful when multiple applications share the same Redis database.

Memory Driver

MemoryDriver stores values in the current process. Capacity and cleanup defaults protect against unbounded growth:

OptionDefaultDescription
maxEntries10_000Maximum keys retained; least-recently-used entries are evicted when full
cleanupIntervalMs60_000Periodic scan that drops expired keys never read again. Use 0 to disable

Reads and overwrites refresh LRU order (via Map insertion order). Expired entries are still removed lazily on get/has. The cleanup timer is unref'd so it does not keep the process alive by itself. app.stop() and graceful shutdown (SIGTERM/SIGINT) call CacheService.close() so the timer and in-memory storage are released with the application.

import { CacheService, MemoryDriver } from '@carno.js/core';

// Defaults: maxEntries=10_000, cleanup every 60s
const cache = new CacheService({
driver: new MemoryDriver(),
});

// Custom capacity and cleanup
const bounded = new CacheService({
driver: new MemoryDriver({
maxEntries: 50_000,
cleanupIntervalMs: 30_000,
}),
});

// Opt out of the timer (lazy expiration only)
const lazyOnly = new CacheService({
driver: new MemoryDriver({ cleanupIntervalMs: 0 }),
});

The positional form new MemoryDriver(cleanupIntervalMs) remains supported for compatibility; prefer the options object for new code.

Use it for:

  • Local development.
  • Tests.
  • Single-instance applications.
  • Short-lived process-local values.

Do not use memory cache as your only shared cache when the application runs multiple instances. Each process has its own isolated memory.

Known risk: TTL units with Redis

Documentation and MemoryDriver treat TTL as milliseconds. RedisDriver currently passes the value to setex, which expects seconds. Mixing defaultTtl: 60_000 across drivers is therefore inconsistent. This divergence is intentional until a dedicated compatibility release; prefer driver-specific TTLs when using Redis.

Redis Driver

Use RedisDriver when cached values must be shared across application instances.

import { CacheService, RedisDriver } from '@carno.js/core';

const cache = new CacheService({
driver: new RedisDriver({
host: 'localhost',
port: 6379,
}),
prefix: 'carno',
defaultTtl: 60_000,
});

Register it as the app-level CacheService:

import { Carno, CacheService, RedisDriver } from '@carno.js/core';

const app = new Carno()
.services([
{
token: CacheService,
useValue: new CacheService({
driver: new RedisDriver({ url: process.env.REDIS_URL }),
prefix: 'my-app',
defaultTtl: 60_000,
}),
},
]);

The Redis driver uses Bun's native Redis client when available and falls back to ioredis if installed.

Custom Drivers

Implement CacheDriver to integrate another backend.

import type { CacheDriver } from '@carno.js/core';

class CustomDriver implements CacheDriver {
readonly name = 'CustomDriver';

async get<T>(key: string): Promise<T | null> {
// read from backend
return null;
}

async set<T>(key: string, value: T, ttl?: number): Promise<boolean> {
// write to backend
return true;
}

async del(key: string): Promise<boolean> {
return true;
}

async has(key: string): Promise<boolean> {
return false;
}

async clear(): Promise<void> {}

async close(): Promise<void> {}
}

Then pass the driver to CacheService.

Practical Guidelines

  • Use stable, namespaced keys such as product:${id}.
  • Choose TTLs based on business tolerance for stale data.
  • Delete or overwrite keys after writes when stale data would be harmful.
  • Use Redis for multi-instance deployments.
  • Avoid caching huge objects unless you know the memory and serialization costs.
  • Keep cache failures from becoming data correctness bugs; the database should remain the source of truth.

See Also