@WithLock Decorator
Declarative locking — add distributed locks with a single decorator.
Basic Usage
typescript
import { Injectable } from '@nestjs/common';
import { WithLock } from '@nestjs-redisx/locks';
import { Order } from './types';
@Injectable()
export class OrderService {
@WithLock({ key: 'order:{0}', ttl: 10000 })
async processOrder(orderId: string): Promise<Order> {
// This method is protected by a distributed lock
// Only one instance can process this orderId at a time
return this.doProcess(orderId);
}
private async doProcess(orderId: string): Promise<Order> {
return { id: orderId, amount: 0, status: 'processed' };
}
}Options Reference
typescript
interface IWithLockOptions {
key: string | ((...args: unknown[]) => string); // Lock key or key builder
ttl?: number; // Lock TTL (ms)
waitTimeout?: number; // Max wall-clock time to wait (ms)
autoRenew?: boolean; // Enable auto-renewal
onLockFailed?: 'throw' | 'skip' | ((key: string) => Error); // Failure strategy
}waitTimeout bounds the total wait
waitTimeout caps how long acquisition waits for a contended lock. The service retries with exponential backoff (retry.maxRetries, retry.initialDelay, retry.multiplier, retry.maxDelay) and stops as soon as either the retry cap is reached or waiting again would exceed waitTimeout, whichever comes first. Leave waitTimeout unset to let the retry config alone govern the wait.
Key Patterns
Static Key
typescript
// Global lock — only one instance runs at a time
@WithLock({ key: 'sync:products' })
async syncProducts(): Promise<void> {
// Global product sync
}Parameter-Based Key
typescript
// {0} = first argument
@WithLock({ key: 'user:{0}' })
async updateUser(userId: string, data: UpdateDto): Promise<User> {
// Lock per user
}
// {1} = second argument
@WithLock({ key: 'order:{0}:item:{1}' })
async updateOrderItem(orderId: string, itemId: string): Promise<void> {
// Lock per order+item combination
}Object Property Key
typescript
// Access DTO properties
@WithLock({ key: 'payment:{0.orderId}' })
async processPayment(dto: PaymentDto): Promise<Payment> {
return this.paymentGateway.charge(dto);
}Dynamic Key Function
typescript
@WithLock({
key: (userId, action) => `user:${userId}:${action}`,
ttl: 5000,
})
async performAction(userId: string, action: string): Promise<void> {
// Custom key logic
}TTL Configuration
typescript
// Short operation
@WithLock({ key: 'quick:{0}', ttl: 5000 }) // 5 seconds
async quickUpdate(id: string) { }
// Long operation with auto-renewal
@WithLock({ key: 'long:{0}', ttl: 60000, autoRenew: true }) // 1 min + renew
async longProcess(id: string) { }Error Handling
Throw on Failure (Default)
typescript
@WithLock({ key: 'payment:{0}', onLockFailed: 'throw' })
async processPayment(orderId: string) {
// Throws LockAcquisitionError if lock not acquired
}Skip on Failure
typescript
@WithLock({ key: 'optional:{0}', onLockFailed: 'skip' })
async optionalTask(id: string) {
// Returns undefined if lock not acquired
// Method not executed
}Custom Error
typescript
@WithLock({
key: 'payment:{0}',
onLockFailed: (key) => new ConflictException(`Order ${key} already being processed`)
})
async processPayment(orderId: string) {
// Throws custom error
}Retry Behavior
The @WithLock decorator uses the global retry settings configured in LocksPlugin.
Per-operation retry control
For per-operation retry overrides, use the Service API directly:
typescript
const lock = await this.lockService.acquire('key', {
retry: { maxRetries: 5, initialDelay: 200 },
});Real-World Examples
Payment Processing
typescript
@WithLock({
key: 'payment:order:{0}',
ttl: 30000,
})
async processPayment(orderId: string): Promise<Payment> {
const order = await this.orders.findOne(orderId);
if (order.paid) {
throw new BadRequestException('Already paid');
}
const result = await this.gateway.charge(order);
await this.orders.markPaid(orderId);
return result;
}Inventory Management
typescript
@WithLock({ key: 'inventory:{0}', ttl: 5000 })
async reserveStock(sku: string, quantity: number): Promise<boolean> {
const stock = await this.inventory.getStock(sku);
if (stock < quantity) {
return false;
}
await this.inventory.decrement(sku, quantity);
return true;
}Leader Election
typescript
@WithLock({
key: 'leader:scheduler',
ttl: 60000,
autoRenew: true,
})
async runScheduler(): Promise<void> {
// Only one instance becomes leader
while (this.running) {
await this.processScheduledJobs();
await this.sleep(10000);
}
}Best Practices
Do
typescript
// Use specific keys
@WithLock({ key: 'user:{0}:profile' })
// Set appropriate TTL
@WithLock({ key: 'task:{0}', ttl: 10000 })
// Enable auto-renew for long tasks
@WithLock({ key: 'report:{0}', ttl: 60000, autoRenew: true })Don't
typescript
// Too generic key
@WithLock({ key: 'lock' }) // Blocks everything!
// TTL too short
@WithLock({ key: 'slow:{0}', ttl: 1000 }) // 1s for slow operation
// Missing auto-renew for long operation
@WithLock({ key: 'export:{0}', ttl: 30000 }) // May expire mid-exportNext Steps
- Service API — Programmatic lock management
- Auto-Renewal — How auto-renewal works