Stores
Share circuit-breaker state across instances with a pluggable ResilienceStore — in-memory by default, or Redis / Postgres / SQLite adapters that coordinate the half-open probe atomically.
The circuit breaker keeps its state — failure counts, open-until timestamps, the half-open probe — behind a small ResilienceStore interface. Swap the implementation to move that state from one process to your whole fleet.
interface ResilienceStore {
admit(key: string, cfg: BreakerConfig): Promise<Admission>; // may I run? (and am I the probe?)
record(key: string, cfg: BreakerConfig, ok: boolean, probe: boolean): Promise<CircuitStatus>;
snapshot(key: string): Promise<CircuitSnapshot>; // read-only inspect
}admit and record must be atomic — the whole point of a distributed store is that, under concurrent load, exactly one instance gets the half-open probe. Every adapter below enforces that against its real engine (Lua, SELECT … FOR UPDATE, a SQLite transaction) and is verified by a shared contract suite.
In-memory (default)
InMemoryResilienceStore ships in the core package and needs nothing. State lives in the process, so each instance trips its own breaker:
import { InMemoryResilienceStore } from '@dudousxd/nestjs-resilience';
const store = new InMemoryResilienceStore(); // accepts an optional Clock for testsDistributed adapters
Each adapter is its own package with the engine driver as a peer dependency. Pick one:
| Package | Engine | Construct | Schema |
|---|---|---|---|
@dudousxd/nestjs-resilience-store-redis | Redis (ioredis) | new RedisResilienceStore(redis, opts?) | none (atomic Lua) |
@dudousxd/nestjs-resilience-store-typeorm | Postgres (TypeORM) | new TypeOrmResilienceStore(dataSource, opts?) | ensureSchema() |
@dudousxd/nestjs-resilience-store-prisma | Postgres (Prisma) | new PrismaResilienceStore(prisma, opts?) | ensureSchema() |
@dudousxd/nestjs-resilience-store-mikro-orm | Postgres (MikroORM) | new MikroOrmResilienceStore(orm, opts?) | ensureSchema() |
@dudousxd/nestjs-resilience-store-drizzle | SQLite (better-sqlite3) | new DrizzleResilienceStore(db, opts?) | exported schema |
pnpm add @dudousxd/nestjs-resilience-store-redis ioredisimport Redis from 'ioredis';
import { ResilienceModule } from '@dudousxd/nestjs-resilience';
import { RedisResilienceStore } from '@dudousxd/nestjs-resilience-store-redis';
ResilienceModule.forRootAsync({
inject: [Redis],
useFactory: (redis: Redis) => ({
store: new RedisResilienceStore(redis, {
keyPrefix: 'resilience:cb:', // default
}),
}),
});admit/record each run as a single atomic Lua script — no schema, no migration.
pnpm add @dudousxd/nestjs-resilience-store-typeorm typeormimport { DataSource } from 'typeorm';
import { ResilienceModule } from '@dudousxd/nestjs-resilience';
import { TypeOrmResilienceStore } from '@dudousxd/nestjs-resilience-store-typeorm';
ResilienceModule.forRootAsync({
inject: [DataSource],
useFactory: async (dataSource: DataSource) => {
const store = new TypeOrmResilienceStore(dataSource);
await store.ensureSchema(); // create the circuits table once on boot
return { store };
},
});Atomicity uses a transaction with SELECT … FOR UPDATE. Prefer migrations? Import the CIRCUITS_DDL string instead of calling ensureSchema().
pnpm add @dudousxd/nestjs-resilience-store-prisma @prisma/clientimport { PrismaService } from './prisma.service';
import { ResilienceModule } from '@dudousxd/nestjs-resilience';
import { PrismaResilienceStore } from '@dudousxd/nestjs-resilience-store-prisma';
ResilienceModule.forRootAsync({
inject: [PrismaService],
useFactory: async (prisma: PrismaService) => {
const store = new PrismaResilienceStore(prisma);
await store.ensureSchema();
return { store };
},
});The store duck-types the client ($transaction / $executeRawUnsafe / $queryRawUnsafe), so it works across Prisma 5–7 without importing your generated client.
pnpm add @dudousxd/nestjs-resilience-store-mikro-orm @mikro-orm/core @mikro-orm/postgresqlimport { MikroORM } from '@mikro-orm/core';
import { ResilienceModule } from '@dudousxd/nestjs-resilience';
import { MikroOrmResilienceStore } from '@dudousxd/nestjs-resilience-store-mikro-orm';
ResilienceModule.forRootAsync({
inject: [MikroORM],
useFactory: async (orm: MikroORM) => {
const store = new MikroOrmResilienceStore(orm);
await store.ensureSchema();
return { store };
},
});Uses em.transactional() — make sure transactions aren't disabled on your config.
pnpm add @dudousxd/nestjs-resilience-store-drizzle drizzle-orm better-sqlite3import { ResilienceModule } from '@dudousxd/nestjs-resilience';
import { DrizzleResilienceStore, resilienceSchema } from '@dudousxd/nestjs-resilience-store-drizzle';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
ResilienceModule.forRootAsync({
useFactory: () => {
const db = drizzle(new Database('app.db'), { schema: resilienceSchema });
return { store: new DrizzleResilienceStore(db) };
},
});SQLite-backed with synchronous transactions. The package exports circuits, resilienceSchema, and CIRCUITS_DDL for your migrations.
Wiring a store
Pass the store to the module — every breaker and decorator then shares it:
ResilienceModule.forRootAsync({
inject: [Redis],
useFactory: (redis: Redis) => ({ store: new RedisResilienceStore(redis) }),
});…or to an individual circuitBreaker({ store, … }) / failover({ policy: () => circuitBreaker({ store, … }) }).
All adapters share one type, BreakerConfig (threshold, cooldownMs, halfOpenMax?) — the same options you pass to circuitBreaker. Bring your own engine by implementing the three methods and validating it against the contract suite.
Building a custom store
Your engine isn't in the table above? A store is just the three admit / record / snapshot methods, and the core package gives you everything to implement them without re-deriving the breaker's state machine. There are two paths depending on whether your engine speaks SQL.
Path A — reuse the SQL base (any relational engine)
Every relational adapter (store-typeorm, store-prisma, store-mikro-orm) is a few lines on top of the exported SqlResilienceStore. It owns the atomic load → compute → persist cycle, the dialect-aware statements, and the resilience_circuits DDL — so all you supply is a tiny SqlDriver: your placeholder dialect plus how to run a transaction, a plain read, and a DDL statement.
export type SqlPlaceholderStyle = 'numbered' | 'positional'; // `$1,$2…` (Postgres) vs `?`
/** A parameterized statement runner scoped to one transaction. */
interface SqlTx {
run(sql: string, params: unknown[]): Promise<void>; // INSERT/UPDATE, result discarded
all(sql: string, params: unknown[]): Promise<unknown[]>; // SELECT, returns rows
}
/** The whole per-engine contract. Everything else lives in SqlResilienceStore. */
interface SqlDriver {
readonly placeholders: SqlPlaceholderStyle;
transaction<R>(body: (tx: SqlTx) => Promise<R>): Promise<R>; // MUST allow `SELECT … FOR UPDATE`
read(sql: string, params: unknown[]): Promise<unknown[]>; // non-transactional, for snapshot()
exec(sql: string): Promise<void>; // run a DDL statement
}
interface SqlResilienceStoreOptions {
clock?: Clock; // inject a FakeClock in tests; defaults to systemClock
}Here's the entire TypeORM adapter — your own engine follows the same shape, only the four driver methods change:
import { SqlResilienceStore, type SqlDriver, type SqlResilienceStoreOptions } from '@dudousxd/nestjs-resilience';
import type { DataSource } from 'typeorm';
export class TypeOrmResilienceStore extends SqlResilienceStore {
constructor(ds: DataSource, opts: SqlResilienceStoreOptions = {}) {
super(typeOrmDriver(ds), opts);
}
}
function typeOrmDriver(ds: DataSource): SqlDriver {
return {
placeholders: 'numbered', // Postgres → $1, $2, …
transaction: (body) =>
ds.transaction((em) =>
body({
run: async (sql, params) => { await em.query(sql, params); },
all: (sql, params) => em.query(sql, params) as Promise<unknown[]>,
}),
),
read: (sql, params) => ds.query(sql, params) as Promise<unknown[]>,
exec: async (sql) => { await ds.query(sql); },
};
}Atomicity lives in your transaction. SqlResilienceStore issues a SELECT … FOR UPDATE inside the callback and relies on it holding a row lock for the duration of the transaction — that's what hands the half-open probe to exactly one instance. If your engine can't lock the row (or serialize the transaction), the store is not safe under concurrency.
The DDL is exposed as CIRCUITS_DDL (a plain, dialect-agnostic CREATE TABLE IF NOT EXISTS string) and applied by ensureSchema(). Call it once on boot, or feed CIRCUITS_DDL to your migration tool instead:
import { CIRCUITS_DDL, SqlResilienceStore } from '@dudousxd/nestjs-resilience';
const store = new SqlResilienceStore(myDriver);
await store.ensureSchema(); // runs CIRCUITS_DDL — safe to call on every startup
// …or in a migration: await queryRunner.query(CIRCUITS_DDL);Path B — implement ResilienceStore directly (non-SQL engines)
For a key/value or document engine, implement the interface yourself — but don't reinvent the breaker logic. The core package exports the state machine as three pure, serializable primitives, so your store only handles storage and atomicity:
| Export | What it is |
|---|---|
CircuitState | The plain, serializable unit every store persists: { status, failures, openUntil, probes }. |
INITIAL_CIRCUIT_STATE | Fresh-circuit defaults (a brand-new key behaves as a closed circuit). |
computeAdmit(prev, cfg, now) | Pure "may I run?" decision → { state, admission }. No I/O. |
computeRecord(prev, cfg, ok, probe, now) | Pure "record this outcome" → { state, status }. No I/O. |
The pattern is always load → compute → persist, wrapped in whatever atomicity your engine provides. This is exactly how the synchronous Drizzle/SQLite adapter works:
import {
INITIAL_CIRCUIT_STATE, computeAdmit, computeRecord, systemClock,
type Admission, type BreakerConfig, type CircuitSnapshot, type CircuitState,
type CircuitStatus, type Clock, type ResilienceStore,
} from '@dudousxd/nestjs-resilience';
export class MyKvResilienceStore implements ResilienceStore {
private readonly clock: Clock;
constructor(private readonly engine: MyEngine, opts: { clock?: Clock } = {}) {
this.clock = opts.clock ?? systemClock;
}
private load(key: string): CircuitState {
const row = this.engine.get(key);
return row ? (JSON.parse(row) as CircuitState) : { ...INITIAL_CIRCUIT_STATE };
}
async admit(key: string, cfg: BreakerConfig): Promise<Admission> {
return this.engine.transaction(() => { // ← your atomic section
const { state, admission } = computeAdmit(this.load(key), cfg, this.clock.now());
this.engine.set(key, JSON.stringify(state));
return admission;
});
}
async record(key: string, cfg: BreakerConfig, ok: boolean, probe: boolean): Promise<CircuitStatus> {
return this.engine.transaction(() => {
const { state, status } = computeRecord(this.load(key), cfg, ok, probe, this.clock.now());
this.engine.set(key, JSON.stringify(state));
return status;
});
}
async snapshot(key: string): Promise<CircuitSnapshot> {
const s = this.load(key);
return { status: s.status, failures: s.failures, ...(s.openUntil ? { openUntil: s.openUntil } : {}) };
}
}Threading a Clock through (defaulting to systemClock) lets a FakeClock drive cooldowns instantly in tests. Whichever path you take, prove it against the shared contract suite — it's the same five-behaviour test every built-in adapter passes, including the concurrent-probe and lost-update checks.
Decorators & Module
Wrap provider methods with @Timeout / @Retry / @CircuitBreaker, register named policies through ResilienceModule, and run them via ResilienceService.
Integrations
Emit state transitions over nestjs-diagnostics, mirror them onto @nestjs/event-emitter, and make breaker keys tenant-aware through nestjs-context — all soft-detected and optional.