Skip to content

Recipes

Fallback to a cached value

Return a cached/default value instead of throwing while the breaker is OPEN — the dependency gets time to recover, users get slightly stale data instead of errors.

typescript
import { Injectable, Inject } from '@nestjs/common';
import { CIRCUIT_BREAKER_SERVICE, ICircuitBreakerService } from '@nestjs-redisx/circuit-breaker';
import { UsersApi, UserCache, User } from '../types';

/**
 * Serve a cached value instead of erroring while the breaker is OPEN.
 * The dependency gets time to recover; users get slightly stale data.
 */
@Injectable()
export class UsersWithFallbackService {
  constructor(
    @Inject(CIRCUIT_BREAKER_SERVICE)
    private readonly breaker: ICircuitBreakerService,
    private readonly usersApi: UsersApi,
    private readonly userCache: UserCache,
  ) {}

  async getUser(id: string): Promise<User> {
    return this.breaker.execute(`users-api:${id}`, () => this.usersApi.getUser(id), {
      fallback: async () => {
        const cached = await this.userCache.get(id);
        return cached ?? { id, name: 'unknown' };
      },
    });
  }
}

One breaker per dependency (or per tenant)

Use a stable key per dependency so failures of one dependency never trip another, and interpolate arguments for finer-grained circuits.

typescript
import { Injectable } from '@nestjs/common';
import { WithCircuitBreaker } from '@nestjs-redisx/circuit-breaker';
import { PaymentGateway, UsersApi, Charge, User } from '../types';

/**
 * One breaker per dependency: a Stripe outage never trips the users-api
 * circuit. Interpolate arguments for finer-grained (per-user / per-tenant)
 * breakers.
 */
@Injectable()
export class PerDependencyService {
  constructor(
    private readonly gateway: PaymentGateway,
    private readonly usersApi: UsersApi,
  ) {}

  // Static key: all charge() calls share the 'stripe' circuit.
  @WithCircuitBreaker({ key: 'stripe' })
  async charge(charge: Charge): Promise<{ ok: boolean }> {
    return this.gateway.charge(charge);
  }

  // Template key: separate circuit per user id ({0} = first argument).
  @WithCircuitBreaker({ key: 'users-api:{0}' })
  async getUser(id: string): Promise<User> {
    return this.usersApi.getUser(id);
  }

  // Function key: derive the circuit from a payload field.
  @WithCircuitBreaker({ key: (dto: { tenantId: string }) => `tenant:${dto.tenantId}` })
  async syncTenant(dto: { tenantId: string }): Promise<void> {
    await this.usersApi.getUser(dto.tenantId);
  }
}

Skip instead of throw

For non-critical background work, resolve to undefined while OPEN instead of throwing; for trusted internal traffic, bypass the breaker entirely with skip().

typescript
import { Injectable } from '@nestjs/common';
import { WithCircuitBreaker } from '@nestjs-redisx/circuit-breaker';
import { UsersApi, User } from '../types';

/**
 * Non-critical work: resolve to undefined instead of throwing while OPEN
 * (onOpen: 'skip'), and bypass the breaker entirely for trusted internal
 * calls (skip()).
 */
@Injectable()
export class NonCriticalService {
  constructor(private readonly usersApi: UsersApi) {}

  // Background warmup: silently skipped while the breaker is OPEN.
  @WithCircuitBreaker({ key: 'users-api', onOpen: 'skip' })
  async warmProfileCache(id: string): Promise<User | undefined> {
    return this.usersApi.getUser(id);
  }

  // Internal traffic bypasses the breaker: no state is read or recorded.
  @WithCircuitBreaker({
    key: 'users-api',
    skip: (_id: string, internal?: boolean) => internal === true,
  })
  async getUser(id: string, internal?: boolean): Promise<User> {
    return this.usersApi.getUser(id);
  }
}

Health-probe driven breaker

Drive the breaker from a scheduled health probe with the manual API instead of wrapping every call — hot-path code only reads the state.

typescript
import { Injectable, Inject } from '@nestjs/common';
import { CIRCUIT_BREAKER_SERVICE, ICircuitBreakerService } from '@nestjs-redisx/circuit-breaker';
import { UsersApi } from '../types';

/**
 * Drive the breaker from an external health signal instead of wrapping every
 * call: a scheduled probe records success/failure manually, and hot-path code
 * only reads the state.
 */
@Injectable()
export class HealthProbeService {
  constructor(
    @Inject(CIRCUIT_BREAKER_SERVICE)
    private readonly breaker: ICircuitBreakerService,
    private readonly usersApi: UsersApi,
  ) {}

  /** Call this from a scheduler (e.g. @Cron) every few seconds. */
  async probeUsersApi(): Promise<void> {
    try {
      await this.usersApi.getUser('health-check');
      await this.breaker.recordSuccess('users-api');
    } catch {
      await this.breaker.recordFailure('users-api');
    }
  }

  /** Hot path: consult the circuit without mutating it. */
  async isUsersApiAvailable(): Promise<boolean> {
    const snapshot = await this.breaker.getState('users-api');
    return snapshot.state === 'closed';
  }
}

Inspecting and resetting

Surface a degraded-mode banner from the non-mutating getState, and force a circuit back to CLOSED with reset() after a fix ships.

typescript
import { Injectable, Inject } from '@nestjs/common';
import { CIRCUIT_BREAKER_SERVICE, ICircuitBreakerService } from '@nestjs-redisx/circuit-breaker';

/**
 * Operator tooling: surface a degraded-mode banner while a circuit is not
 * closed, and force a stuck circuit back to CLOSED after remediation
 * (e.g. a deploy fixed the dependency).
 */
@Injectable()
export class BreakerOpsService {
  constructor(
    @Inject(CIRCUIT_BREAKER_SERVICE)
    private readonly breaker: ICircuitBreakerService,
  ) {}

  async degradedBanner(): Promise<string | null> {
    const { state, failuresInWindow } = await this.breaker.getState('stripe');
    if (state === 'open') {
      return 'Payments are temporarily degraded — orders are queued.';
    }
    if (state === 'half-open') {
      return 'Payments are recovering.';
    }
    return failuresInWindow > 0 ? `Payments unstable (${failuresInWindow} recent failures)` : null;
  }

  /** Admin action: clear the circuit immediately after a fix is deployed. */
  async forceClose(): Promise<void> {
    await this.breaker.reset('stripe');
  }
}

Released under the MIT License.