railiz-resilience
v2.0.0
Published
Unified resilience layer with retry, circuit breaker, and caching for reliable backend execution.
Downloads
3,810
Maintainers
Readme
🌐 railiz-resilience
🚀 A unified resilience layer for modern backend systems — retry, breaker, and cache in one pipeline.
Built for retries, circuit breaking, caching, and fault tolerance.
Designed to work standalone or inside the Railiz ecosystem Think: axios-retry + opossum + cache layer — unified in one pipeline
Why Railiz Resilience
- 🔁 Retry with backoff
- 🧠 Circuit breaker
- ⚡ Multi-layer caching (memo + request cache)
- 🏷 Tag-based invalidation
- 🌐 HTTP cache-control helper
- 🧩 Plugin-based architecture
Metal Model
request
↓
┌──────────────┐
│ breaker │
└──────┬───────┘
↓
┌──────────────┐
│ retry │
└──────┬───────┘
↓
┌──────────────┐
│ cache │───── HIT ───► return
└──────┬───────┘
MISS
↓
┌──────────────┐
│ in-flight │
└──────┬───────┘
↓
fn()
↓
result
↓
cache write + tagscircuit breaker → retry → cache (L1 → L2) → in-flight dedupe → execution → result → cache write
Installation
npm install railiz-resilience railizQuick example
import { runWithPolicy, createResilienceContext } from 'railiz-resilience'
const ctx = createResilienceContext()
const data = await runWithPolicy(
ctx,
'jsonplaceholder/posts',
async () => {
const res = await fetch(
'https://jsonplaceholder.typicode.com/posts/1'
)
if (!res.ok) throw new Error('HTTP error')
return res.json()
},
{
retry: { attempts: 2, delay: 100 },
breaker: {
key: 'jsonplaceholder',
fallback: () => ({
id: -1,
title: 'fallback',
}),
},
cache: { ttl: 5000 },
}
)
console.log(data)ResilienceContext
import { createResilienceContext, createMemoryCache } from 'railiz-resilience'
const ctx = createResilienceContext({
id: 'req-1',
set: (key, value) => {
// inject headers
},
// state: {
// cacheDebug: true,
// },
// cacheClient: createMemoryCache(),
// diGet: async (key) => {
// if (key === 'breakerRegistry') {
// return new CircuitBreakerRegistry()
// }
// },
})Cache System
- In-memory cache (per instance)
- Supports TTL expiration
- Supports in-flight deduplication
- Supports tag-based invalidation
⚠️ Note: cache is not distributed (use Redis for multi-node systems)
Cache Layers
- L1: in-memory (fast, per instance)
- L2: adapter (optional, e.g. Redis)
- Timeout errors are treated as failures (affects retry + circuit breaker)
Flow:
L1 (fresh) → return
L1 (stale + SWR) → return stale + revalidate background
L2 → return
→ fn → write backMemo (L1 Cache)
const memoFn = memo()
app.get('/memo/user/:id', async (ctx) => {
const user = await memoFn(`user:${ctx.params.id}`, async () => {
console.log('DB HIT')
return { id: ctx.params.id, name: 'John' }
}, { ttl: 10000 })
ctx.json(user)
})
HTTP Cache Control (Browser / CDN)
Supports:
- public / private
- max-age
- stale-while-revalidate
- no-store / no-cache
app.get('/public/users/:id', async (ctx) => {
const cc = cacheControl(ctx)
cc({
public: true,
maxAge: 60000,
swr: 30000,
})
const user = await fetch(
`https://jsonplaceholder.typicode.com/users/${ctx.params.id}`,
).then(r => r.json())
ctx.json(user)
})
Cache Tags
await runWithPolicy(ctx, 'user:1', fetchUser, {
cache: {
ttl: 60000,
tags: ['user'],
},
})
invalidateTag('user')Real Examples
External API
const results = await runWithPolicy(ctx, 'api:github', async () => {
const res = await fetch('https://api.github.com/users/octocat')
if (!res.ok) throw new Error('GitHub error')
return res.json()
}, {
retry: { attempts: 3, delay: 200 },
breaker: { key: 'github-api' },
})
console.log(results)Database
await runWithPolicy(ctx, 'db:user', () => db.users.findById(1), {
retry: { attempts: 2 },
breaker: { key: 'db-users' },
})Payment (critical service)
await runWithPolicy(ctx, 'payment:charge', async () => {
const res = await fetch('https://payment/api/charge', {
method: 'POST',
})
if (!res.ok) throw new Error('payment failed')
return res.json()
}, {
retry: { attempts: 5, delay: 300 },
breaker: {
key: 'payment-service',
fallback: () => ({
status: 'PENDING',
}),
},
})Queue / Email
await runWithPolicy(ctx, 'queue:email', () => sendEmail(), {
retry: { attempts: 5, delay: 500 },
})Circuit Breaker States
States:
- CLOSED
- OPEN
- HALF_OPEN
Supports:
- failureThreshold
- resetTimeout
- fallback
Timeout Behavior
- Timeout is applied per retry attempt
- Each retry execution is individually time-bounded
Retry Behavior
- Exponential backoff supported
- Skips AbortError
- Retries per attempt boundary
Abort Handling
- AbortError is not retried
- Propagates immediately to caller
Why it matters
- Prevent cascading failures
- Protect external services
- Stabilize distributed systems
Policy Options
// runWithPolicy (options)
type PolicyOptions<T> = {
timeout?: number
retry?: {
attempts?: number
delay?: number
}
breaker?: {
key?: string
fallback?: (err: any) => T | Promise<T>
failureThreshold?: number
resetTimeout?: number
}
cache?: {
ttl?: number
swr?: number
key?: string
tags?: string | string[]
debug?: boolean
onEvent?: (event: 'hit' | 'miss' | 'stale' | 'write') => void
}
}Full Example
const data = await runWithPolicy(ctx, 'example:key', fetchData, {
timeout: 1000,
retry: {
attempts: 2,
delay: 100,
},
breaker: {
key: 'example-service',
failureThreshold: 3,
resetTimeout: 5000,
fallback: () => ({
ok: false,
fallback: true,
}),
},
cache: {
ttl: 5000,
swr: 20000,
tags: ['example'],
debug: true,
onEvent: (e) => console.log('cache:', e),
},
})Railiz Integration
import { Railiz } from 'railiz'
import { resiliencePlugin, invalidateTag } from 'railiz-resilience'
const app = new Railiz()
app.plugin(
// { failureThreshold = 5, resetTimeout = 30000 } => default
resiliencePlugin({
breaker: {
failureThreshold: 3,
resetTimeout: 15000,
},
}),
)
app.get('/', async (ctx) => {
ctx.json({
hello: "hello"
})
})
app.get('/users/:id', async (ctx) => {
const user = await runWithPolicy(ctx, 'user', fetchUser, {
timeout: 1500,
retry: { attempts: 2 },
cache: { ttl: 10000, tags: ['users'], },
breaker: { key: 'user-service' },
// more
})
ctx.json(user)
})
app.post('/invalidate/users', async (ctx) => {
invalidateTag('users')
ctx.json({ ok: true })
})
app.run(3000, () => {
console.log('Server running on http://localhost:3000')
})Standalone Usage
runWithPolicy works without any framework.
resiliencePlugin is optional and only needed for:
- shared breaker registry
- framework integration
Comparison
| Criteria | railiz-resilience | axios-retry | opossum | |------------------|------------------|------------|--------| | Retry | ✅ | ✅ | ❌ | | Circuit Breaker | ✅ | ❌ | ✅ | | Cache | ✅ | ❌ | ❌ | | SWR | ✅ | ❌ | ❌ | | Tag invalidation | ✅ | ❌ | ❌ |
Performance
- O(1) cache lookup
- Minimal overhead per request
- Zero dependency core
When to use
- Microservices
- External APIs
- Databases
- Job queues
- Distributed systems
When NOT to use
- Sync logic
- Static data
- Simple scripts
Philosophy
"Failure is inevitable — resilience should be built-in."
License
MIT
