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');
}
}