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.