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. Bounded-Wait Pattern

Bound how long acquisition waits before giving up. Use waitTimeout for a hard wall-clock cap, and/or the retry config (maxRetries with exponential backoff) to shape the retry schedule. Acquisition stops as soon as either limit is hit:

typescript
// Hard cap: wait at most ~2s for the lock, then throw
@WithLock({ key: 'resource:{0}', waitTimeout: 2000 })
async accessResource(id: string) {
  // Throws LockAcquisitionError once waitTimeout (or the retry cap) is reached
}

// Or shape the retry schedule globally on the plugin
new LocksPlugin({
  retry: {
    maxRetries: 5,
    initialDelay: 100,
    multiplier: 2,
    maxDelay: 2000,
  },
})

TIP

waitTimeout is the simplest way to bound waiting — the service stops retrying once the next backoff sleep would exceed it. Combine it with the retry settings, or rely on the retry settings alone if you do not set waitTimeout.

Next Steps

Released under the MIT License.