@develop-x/nest-lock
v1.0.6
Published
A NestJS Redis-based distributed lock and rate limiting library that provides both decorators and services for preventing concurrent execution and rate limiting in distributed applications.
Keywords
Readme
@develop-x/nest-lock
A NestJS Redis-based distributed lock and rate limiting library that provides both decorators and services for preventing concurrent execution and rate limiting in distributed applications.
Features
- Distributed Locking: Prevent concurrent execution of critical sections across multiple instances
- Rate Limiting: Control the frequency of operations per user/key
- Decorator Support: Easy-to-use decorators for method-level protection
- Service Classes: Programmatic access to locking and rate limiting functionality
- Redis Backend: Uses Redis for distributed coordination
- TypeScript Support: Full TypeScript definitions included
Installation
npm install @develop-x/nest-lockDependencies
This library requires the following peer dependencies:
@nestjs/common ^11.0.1@nestjs/core ^11.0.1
The following dependencies are included:
ioredis ^5.6.1- Redis clientredlock ^5.0.0-beta.2- Distributed locking implementationrate-limiter-flexible ^7.1.1- Rate limiting implementation
Setup
1. Import the Module
import { Module } from '@nestjs/common';
import { LockModule } from '@develop-x/nest-lock';
@Module({
imports: [
LockModule.forRootAsync({
useFactory: () => ({
host: 'localhost',
port: 6379,
// other Redis options
}),
inject: [], // optional dependencies
}),
],
})
export class AppModule {}2. Inject Services in Your Classes
import { Injectable } from '@nestjs/common';
import { RedisLockService, RedisLimitService } from '@develop-x/nest-lock';
@Injectable()
export class UserService {
constructor(
private readonly redisLockService: RedisLockService,
private readonly redisLimitService: RedisLimitService,
) {}
}Usage
Distributed Locking
Using the Decorator
import { RedisLock } from '@develop-x/nest-lock';
@Injectable()
export class PaymentService {
constructor(private readonly redisLockService: RedisLockService) {}
// Lock by specific key
@RedisLock({
key: 'payment-processing',
ttl: 5000, // 5 seconds
})
async processPayment(paymentData: any) {
// This method will be locked for 5 seconds
// Only one instance can execute this at a time
}
// Lock by extracting values from arguments
@RedisLock({
keys: ['userId'], // Extract userId from the first argument object
ttl: 3000,
})
async updateUserBalance(userData: { userId: string, amount: number }) {
// Locked per userId - each user can have one concurrent update
}
// Lock with custom key function
@RedisLock({
keyFn: (orderId: string, userId: string) => `order:${orderId}:user:${userId}`,
ttl: 10000,
onError: () => {
throw new Error('Order is being processed by another request');
}
})
async processOrder(orderId: string, userId: string) {
// Custom lock key generation with custom error handling
}
}Using the Service Directly
@Injectable()
export class InventoryService {
constructor(private readonly redisLockService: RedisLockService) {}
async updateStock(productId: string, quantity: number) {
return await this.redisLockService.runWithLock(
`product:${productId}`,
5000, // 5 second TTL
async () => {
// Critical section - only one update per product at a time
const currentStock = await this.getStock(productId);
return await this.setStock(productId, currentStock + quantity);
},
() => {
throw new Error('Product stock is being updated, please try again');
}
);
}
}Rate Limiting
Using the Service
@Injectable()
export class ApiService {
constructor(private readonly redisLimitService: RedisLimitService) {}
async createPost(userId: string, postData: any) {
return await this.redisLimitService.runWithLimit(
`user:${userId}:create-post`,
async (rateInfo) => {
// This will be executed if rate limit is not exceeded
console.log(`Remaining requests: ${rateInfo.remainingPoints}`);
return await this.savePost(postData);
},
{
points: 5, // 5 requests
duration: 300, // per 5 minutes (300 seconds)
blockDuration: 600, // block for 10 minutes if exceeded
onLimitFail: (rejRes) => {
throw new Error(`Rate limit exceeded. Try again in ${Math.round(rejRes.msBeforeNext / 1000)} seconds`);
}
}
);
}
async sendMessage(userId: string, message: string) {
return await this.redisLimitService.runWithLimit(
`user:${userId}:send-message`,
async () => {
return await this.deliverMessage(message);
},
{
points: 10, // 10 messages
duration: 60, // per minute
throwOnBlocked: false, // Return null instead of throwing
}
);
}
}Use Cases
1. Payment Processing
Prevent double-spending and concurrent payment processing:
@RedisLock({ keys: ['transactionId'], ttl: 30000 })
async processPayment(payment: { transactionId: string, amount: number }) {
// Ensures only one payment with the same transactionId processes at a time
}2. Inventory Management
Prevent overselling in high-concurrency scenarios:
@RedisLock({ keyFn: (productId) => `inventory:${productId}`, ttl: 5000 })
async purchaseProduct(productId: string, quantity: number) {
// Prevents concurrent stock modifications for the same product
}3. User Account Operations
Prevent concurrent modifications to user accounts:
@RedisLock({ keys: ['userId'], ttl: 10000 })
async updateUserProfile(user: { userId: string, data: any }) {
// Ensures user profile updates are atomic per user
}4. API Rate Limiting
Control API usage per user:
async handleApiRequest(userId: string) {
return await this.redisLimitService.runWithLimit(
`api:${userId}`,
async () => {
return await this.processRequest();
},
{ points: 100, duration: 3600 } // 100 requests per hour
);
}5. Resource Allocation
Limit concurrent access to shared resources:
@RedisLock({ key: 'file-processor', ttl: 60000 })
async processLargeFile(filePath: string) {
// Only allow one file processing operation at a time
}6. Database Migration/Maintenance
Ensure only one maintenance operation runs:
@RedisLock({ key: 'db-maintenance', ttl: 300000 })
async runMaintenanceTask() {
// Prevents multiple instances from running maintenance simultaneously
}Configuration Options
RedisLock Decorator Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| key | string | Static lock key | - |
| keys | string[] | Extract values from method arguments to build key | - |
| keyFn | function | Custom function to generate lock key | - |
| ttl | number | Lock time-to-live in milliseconds | 3000 |
| onError | function | Custom error handler when lock acquisition fails | - |
RedisLimitService Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| points | number | Number of requests allowed | 5 |
| duration | number | Time window in seconds | 300 |
| blockDuration | number | Block duration in seconds when limit exceeded | 600 |
| throwOnBlocked | boolean | Whether to throw exception or return null | true |
| onLimitFail | function | Custom handler when rate limit is exceeded | - |
Error Handling
The library throws HTTP exceptions with status code 429 (Too Many Requests) when:
- Lock acquisition fails (concurrent access detected)
- Rate limit is exceeded
You can customize error handling using the onError and onLimitFail options.
Best Practices
- Choose appropriate TTL values: Set lock TTL slightly longer than expected operation time
- Use meaningful lock keys: Include relevant identifiers to scope locks appropriately
- Handle lock failures gracefully: Provide user-friendly error messages
- Monitor Redis performance: High lock contention may indicate design issues
- Use rate limiting for public APIs: Protect against abuse and ensure fair usage
- Combine with circuit breakers: For additional resilience in distributed systems
License
This library is part of the @develop-x namespace and follows the project's licensing terms.
