Skip to content

@WithCircuitBreaker Decorator

@WithCircuitBreaker wraps a method so every call goes through the breaker for a given key. Unlike a NestJS guard, it is proxy-based and works on any Injectable method — services, repositories, gateways — not just controllers (the same pattern as @WithLock and @Cached).

Usage

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

@Injectable()
export class PaymentsService {
  constructor(
    private readonly gateway: PaymentGateway,
    private readonly usersApi: UsersApi,
    private readonly userCache: UserCache,
  ) {}

  // Trips after repeated failures; throws CircuitBreakerOpenError while OPEN.
  @WithCircuitBreaker({ key: 'stripe', failureThreshold: 5, openDurationMs: 30000 })
  async charge(charge: Charge): Promise<{ ok: boolean }> {
    return this.gateway.charge(charge);
  }

  // Key interpolated from the first argument; falls back to cache when OPEN.
  @WithCircuitBreaker({
    key: 'users-api:{0}',
    fallback: (id: string) => ({ id, name: 'cached' }),
  })
  async getUser(id: string): Promise<User> {
    return this.usersApi.getUser(id);
  }

  // Skip execution (resolve to undefined) instead of throwing while OPEN.
  @WithCircuitBreaker({ key: 'users-api', onOpen: 'skip' })
  async warmCache(id: string): Promise<User | null> {
    return this.userCache.get(id);
  }

  // Bypass the breaker entirely for trusted/internal calls via skip().
  @WithCircuitBreaker({
    key: 'users-api',
    skip: (id: string, internal?: boolean) => internal === true,
  })
  async getUserMaybeInternal(id: string, internal?: boolean): Promise<User> {
    return this.usersApi.getUser(id);
  }
}

Options

IWithCircuitBreakerOptions:

OptionTypeDescription
keystring | (...args) => stringCircuit key. Strings support {0}, {1.id} interpolation from arguments.
failureThresholdnumberPer-method override.
windowMsnumberPer-method override.
openDurationMsnumberPer-method override.
halfOpenMaxCallsnumberPer-method override.
successThresholdnumberPer-method override.
probeTimeoutMsnumberPer-method override (defaults to the resolved openDurationMs).
fallback(...args) => unknownCalled with the original arguments when the breaker rejects; its return becomes the method result.
onOpen'throw' | 'skip'When there is no fallback: throw CircuitBreakerOpenError (default) or skip and resolve to undefined.
skip(...args) => boolean | Promise<boolean>Evaluated with the method arguments; when it returns true the method runs directly, bypassing the breaker (no state read or recorded).

Behaviour

  • CLOSED — the method runs normally; failures (thrown errors) are counted.
  • OPEN — the method is not executed. With a fallback, its value is returned; with onOpen: 'skip', undefined is returned; otherwise CircuitBreakerOpenError is thrown.
  • HALF_OPEN — a limited number of calls are allowed through as probes.

TIP

The decorator throwing behaviour comes from the breaker rejecting the call. Recording of success/failure is automatic — a resolved method records success, a thrown error records failure.

WARNING

If the plugin has not finished initializing (no service available yet), the method runs without the breaker and a warning is logged — calls are never blocked by a missing breaker.

Known typing limitation

fallback and skip are typed as (...args: unknown[]) so a single decorator works for any method signature (the same trade-off as @WithLock's key builder). Under strict TypeScript, annotate the callback parameters as unknown and narrow inside, or cast: skip: ((id: string) => boolean) as (...args: unknown[]) => boolean.

Released under the MIT License.