@dtjldamien/nestjs-cache-service
v1.0.2
Published
NestJS cache service with write-locking to prevent thundering herd
Maintainers
Readme
nestjs-cache-service
A NestJS cache service with write-locking to prevent the thundering herd problem.
Features
- Write-locking: Prevents multiple concurrent requests from triggering duplicate expensive operations
- Cache-aside pattern: Automatic cache population with
getOrSet() - Multiple storage backends: Works with Redis, PostgreSQL, MySQL, MongoDB, SQLite, Memcached, and more via Keyv adapters
- Multi-layer caching: Support for L1 (in-memory) and L2 (persistent) cache layers
- TypeScript: Full type safety and IntelliSense support
- Test coverage: Comprehensive test coverage
Installation
# Core dependencies
pnpm add @dtjldamien/nestjs-cache-service @nestjs/cache-manager cache-manager keyv
# Storage adapters (choose based on your needs)
pnpm add @keyv/redis # For Redis
pnpm add @keyv/postgres # For PostgreSQL
pnpm add @keyv/mysql # For MySQL
pnpm add @keyv/mongo # For MongoDB
pnpm add @keyv/sqlite # For SQLiteQuick Start
1. Import the CacheModule
import { Module } from '@nestjs/common';
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => ({
// In-memory cache (default)
}),
}),
],
})
export class AppModule {}2. Use CacheService in your services
import { Injectable } from '@nestjs/common';
import { CacheService } from '@dtjldamien/nestjs-cache-service';
@Injectable()
export class UserService {
constructor(private cacheService: CacheService) {}
async getUser(userId: string) {
return this.cacheService.getOrSet(
`user:${userId}`,
async () => {
// This expensive operation will only run once
// even if multiple requests arrive simultaneously
return this.fetchUserFromDatabase(userId);
},
3600000, // 1 hour TTL
);
}
private async fetchUserFromDatabase(userId: string) {
// Your database query here
}
}Configuration
Storage Options
This package uses Keyv for storage management, supporting multiple backends including Redis, PostgreSQL, MySQL, MongoDB, SQLite, and more. See the Keyv documentation for all available adapters.
Redis Configuration Examples
Basic Redis Setup
import { Module } from '@nestjs/common';
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => ({
stores: [new KeyvRedis('redis://localhost:6379')],
ttl: 3600000, // 1 hour
}),
}),
],
})
export class AppModule {}Redis with Options
import { Module } from '@nestjs/common';
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => ({
stores: [
new Keyv({
store: new KeyvRedis({
url: 'redis://localhost:6379',
// Additional Redis options
socket: {
connectTimeout: 5000,
keepAlive: true,
},
}),
ttl: 3600000,
}),
],
}),
}),
],
})
export class AppModule {}Environment-based Configuration
import { Module } from '@nestjs/common';
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => {
// Use in-memory cache for testing
if (process.env.NODE_ENV === 'test') {
return {};
}
// Use Redis for production
return {
stores: [
new Keyv({
store: new KeyvRedis(`redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`),
}),
],
};
},
}),
],
})
export class AppModule {}Multi-Layer Caching
Combine multiple stores for optimal performance with L1 in-memory cache and L2 persistent storage:
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
CacheModule.registerAsync({
useFactory: () => ({
stores: [
// L1: Fast in-memory cache (short TTL)
new Keyv({ ttl: 60000 }), // 1 minute
// L2: Redis for persistence (longer TTL)
new Keyv({
store: new KeyvRedis('redis://localhost:6379'),
ttl: 3600000, // 1 hour
}),
],
}),
}),
],
})
export class AppModule {}With Dependency Injection
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';
@Module({
imports: [
ConfigModule.forRoot(),
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const redisHost = configService.get('REDIS_HOST');
const redisPort = configService.get('REDIS_PORT');
return {
stores: [
new Keyv({
store: new KeyvRedis(`redis://${redisHost}:${redisPort}`),
}),
],
ttl: 3600000, // Default 1 hour
};
},
}),
],
})
export class AppModule {}API Reference
CacheService
get<T>(key: string): Promise<T | null>
Get a value from cache. If a write operation is in progress for this key, this method will wait for the write to complete before reading.
const value = await cacheService.get<UserData>('user:123');set(key: string, value: any, ttl: number): Promise<void>
Set a value in cache with TTL (in milliseconds). This operation is protected by a write lock.
await cacheService.set('user:123', userData, 3600000); // 1 hourdel(key: string): Promise<void>
Delete a value from cache.
await cacheService.del('user:123');clear(): Promise<void>
Clear all values from cache.
await cacheService.clear();getOrSet<T>(key: string, factory: () => Promise<T>, ttl: number): Promise<T>
Cache-aside pattern: Get from cache or execute factory function and cache the result.
This method prevents the thundering herd problem. If multiple requests arrive for the same uncached key simultaneously, only one will execute the factory function while others wait.
const userData = await cacheService.getOrSet(
`user:${userId}`,
async () => {
// This will only be called once, even with concurrent requests
return await userRepository.findById(userId);
},
3600000, // 1 hour
);Usage Patterns
Basic Caching
// Manual cache management
const cached = await cacheService.get<string>('my-key');
if (!cached) {
const data = await expensiveOperation();
await cacheService.set('my-key', data, 60000);
return data;
}
return cached;Cache-Aside Pattern (Recommended)
// Automatic cache management with thundering herd prevention
const data = await cacheService.getOrSet('my-key', async () => expensiveOperation(), 60000);Cache Invalidation
// Delete specific key
await cacheService.del('user:123');
// Clear all cache
await cacheService.clear();TTL Examples
// 1 minute
await cacheService.set('key', value, 60 * 1000);
// 1 hour
await cacheService.set('key', value, 60 * 60 * 1000);
// 24 hours
await cacheService.set('key', value, 24 * 60 * 60 * 1000);Thundering Herd Prevention
The thundering herd problem occurs when multiple requests simultaneously attempt to regenerate the same expired cache entry, causing:
- Multiple expensive operations (API calls, database queries)
- Increased load on backend services
- Slower response times
This package solves this by using write locks:
// Without write-locking (BAD):
// 10 simultaneous requests = 10 API calls
// With write-locking (GOOD):
// 10 simultaneous requests = 1 API call
const data = await cacheService.getOrSet(
'expensive-key',
async () => apiCall(), // Called only once
3600000,
);How It Works
First request arrives for uncached key
- Acquires write lock
- Executes factory function
- Caches result
- Releases lock
Concurrent requests for same uncached key
- Wait for write lock
- Check cache again after lock acquired
- Return cached value (no factory execution)
Subsequent requests for cached key
- Return cached value immediately
- No lock acquisition needed
Testing
# Run tests
pnpm test
# Run tests with coverage
pnpm test:cov
# Run tests in watch mode
pnpm test:watchBuilding
# Build the package
pnpm build
# The output will be in the dist/ directoryMigration Guide
From Manual Cache Implementation
Before:
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async getData() {
const cached = await this.cacheManager.get('key');
if (cached) return cached;
const data = await expensiveOp();
await this.cacheManager.set('key', data, 60000);
return data;
}After:
import { CacheService } from '@dtjldamien/nestjs-cache-service';
constructor(private cacheService: CacheService) {}
async getData() {
return this.cacheService.getOrSet(
'key',
async () => expensiveOp(),
60000,
);
}From Internal CacheService
If you're migrating from an internal implementation:
- Update imports:
// Before
import { CacheService } from './shared/services/cache.service';
// After
import { CacheService } from '@dtjldamien/nestjs-cache-service';- Update module imports:
// Before
import { CacheModule } from '@nestjs/cache-manager';
@Module({
imports: [CacheModule.registerAsync({ ... })],
providers: [CacheService],
exports: [CacheService],
})
// After
import { CacheModule } from '@dtjldamien/nestjs-cache-service';
@Module({
imports: [CacheModule.registerAsync({ ... })],
// CacheService is now provided by CacheModule
})- The API remains identical - no code changes needed in services using CacheService
License
MIT
Contributing
We welcome contributions to improve this package. Please follow these guidelines:
Development Setup
- Fork and clone the repository
- Install dependencies:
pnpm install - Create a feature branch:
git checkout -b feature/your-feature-name
Code Standards
- Follow the NestJS module best practices documented in AGENTS.md
- Write TypeScript with strict type safety (no
anytypes) - Maintain 100% test coverage for new features
- Use meaningful commit messages following conventional commits format
Before Submitting
Run tests: Ensure all tests pass
pnpm testCheck coverage: Maintain 100% coverage
pnpm test --coverageBuild successfully: Verify the build works
pnpm buildLint and format: Code should pass linting
pnpm lint
Pull Request Process
- Update the README.md with details of changes if needed
- Add tests for any new functionality
- Ensure the PR description clearly describes the problem and solution
- Reference any related issues in the PR description
Code Review Guidelines
- Code must follow TypeScript best practices (see AGENTS.md)
- All public APIs must have JSDoc comments
- Breaking changes require a major version bump
- New features should include usage examples
For detailed NestJS and TypeScript best practices, see AGENTS.md.
Support
For questions or issues, please open an issue in the repository or contact the maintainers.
