Careeroza
Caching with Redis
CacheModule with a Redis Store
An in-memory cache disappears on restart and is not shared between instances. Production apps use Redis. With @nestjs/cache-manager 3 (NestJS 11), stores are Keyv adapters.
Example
npm i @nestjs/cache-manager cache-manager @keyv/redisExample output
added 9 packages in 4s
src/app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigService } from '@nestjs/config';
import { createKeyv } from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
stores: [createKeyv(config.getOrThrow<string>('REDIS_URL'))],
ttl: 60_000, // default TTL in milliseconds
}),
}),
],
})
export class AppModule {}Cache-aside in a Service
src/products/products.service.ts
@Injectable()
export class ProductsService {
constructor(
private readonly prisma: PrismaService,
@Inject(CACHE_MANAGER) private readonly cache: Cache,
) {}
async findOne(id: number) {
const key = `product:${id}`;
const cached = await this.cache.get<Product>(key);
if (cached) return cached; // HIT
const product = await this.prisma.product.findUniqueOrThrow({ where: { id } });
await this.cache.set(key, product, 5 * 60_000); // MISS: store for 5 minutes
return product;
}
async update(id: number, dto: UpdateProductDto) {
const product = await this.prisma.product.update({ where: { id }, data: dto });
await this.cache.del(`product:${id}`); // invalidate on write
return product;
}
}Example
Cache-aside with TTL and invalidation, using a fake clock so the output is stable:
let now = 0;
const db = new Map<number, { id: number; price: number }>([[7, { id: 7, price: 1299 }]]);
let dbReads = 0;
class TtlCache<V> {
private map = new Map<string, { value: V; expires: number }>();
get(key: string): V | undefined {
const e = this.map.get(key);
if (!e || e.expires <= now) { this.map.delete(key); return undefined; }
return e.value;
}
set(key: string, value: V, ttl: number) { this.map.set(key, { value, expires: now + ttl }); }
del(key: string) { this.map.delete(key); }
}
const cache = new TtlCache<{ id: number; price: number }>();
function findOne(id: number) {
const hit = cache.get('product:' + id);
if (hit) return 'HIT ' + JSON.stringify(hit);
dbReads++;
const row = { ...db.get(id)! };
cache.set('product:' + id, row, 300_000);
return 'MISS ' + JSON.stringify(row);
}
function update(id: number, price: number) { db.set(id, { id, price }); cache.del('product:' + id); }
console.log('t=0s ', findOne(7));
now = 60_000; console.log('t=60s ', findOne(7));
update(7, 1199); console.log('updated price to 1199');
console.log('t=60s ', findOne(7));
now = 400_000; console.log('t=400s ', findOne(7), '(TTL expired)');
console.log('database reads:', dbReads);Output
t=0s MISS {"id":7,"price":1299}
t=60s HIT {"id":7,"price":1299}
updated price to 1199
t=60s MISS {"id":7,"price":1199}
t=400s MISS {"id":7,"price":1199} (TTL expired)
database reads: 3