Skip to content

@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-export

Next Steps

Released under the MIT License.