Skip to content

Patterns

Common patterns for using distributed locks.

1. Mutex Pattern

Basic mutual exclusion:

typescript
@WithLock({ key: 'payment:{0}' })
async processPayment(orderId: string) {
  // Only one instance processes this order
}

2. Leader Election

Elect a single leader among multiple instances:

typescript
import { Injectable } from '@nestjs/common';
import { WithLock } from '@nestjs-redisx/locks';

@Injectable()
export class SchedulerService {
  private running = true;

  @WithLock({
    key: 'leader:scheduler',
    ttl: 60000,
    autoRenew: true,
  })
  async becomeLeader() {
    // Only one instance becomes leader
    while (this.running) {
      await this.runScheduledJobs();
      await this.sleep(10000);
    }
  }

  private async runScheduledJobs() { /* process jobs */ }
  private sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); }
}

3. Resource Pool

Manage limited resources:

typescript
import { Injectable, Inject } from '@nestjs/common';
import { LOCK_SERVICE, ILockService } from '@nestjs-redisx/locks';

@Injectable()
export class WorkerService {
  constructor(
    @Inject(LOCK_SERVICE) private readonly lockService: ILockService,
  ) {}

  async claimWork(jobId: string): Promise<boolean> {
    const lock = await this.lockService.tryAcquire(`job:${jobId}`);

    if (!lock) {
      return false; // Another worker claimed it
    }

    try {
      await this.processJob(jobId);
      return true;
    } finally {
      await lock.release();
    }
  }

  private async processJob(jobId: string) { /* process */ }
}

4. Distributed Semaphore

Allow N concurrent holders:

typescript
async acquireSemaphore(key: string, maxConcurrent: number): Promise<boolean> {
  const count = await this.redis.incr(`semaphore:${key}`);
  
  if (count > maxConcurrent) {
    await this.redis.decr(`semaphore:${key}`);
    return false;
  }
  
  return true;
}

async releaseSemaphore(key: string) {
  await this.redis.decr(`semaphore:${key}`);
}

5. Lock Hierarchy

Prevent deadlocks with ordered locking:

typescript
async transferFunds(fromAccount: string, toAccount: string, amount: number) {
  // Always lock in alphabetical order
  const [first, second] = [fromAccount, toAccount].sort();
  
  const lock1 = await this.lockService.acquire(`account:${first}`);
  try {
    const lock2 = await this.lockService.acquire(`account:${second}`);
    try {
      await this.debit(fromAccount, amount);
      await this.credit(toAccount, amount);
    } finally {
      await lock2.release();
    }
  } finally {
    await lock1.release();
  }
}

6. Try-Lock Pattern

Non-blocking acquisition:

typescript
async tryProcessOrder(orderId: string): Promise<boolean> {
  const lock = await this.lockService.tryAcquire(`order:${orderId}`);
  
  if (!lock) {
    // Order already being processed
    return false;
  }
  
  try {
    await this.process(orderId);
    return true;
  } finally {
    await lock.release();
  }
}

7. Timeout Pattern

Wait for lock with timeout:

typescript
@WithLock({
  key: 'resource:{0}',
  waitTimeout: 5000,  // Wait max 5 seconds
})
async accessResource(id: string) {
  // Uses global retry settings
  // Throws LockAcquisitionError if cannot acquire within timeout
}

Next Steps

Released under the MIT License.