Retry Policies & Circuit Breaker
Network instability is inevitable in distributed systems. This library provides two complementary resilience mechanisms: Retry Policies for transient failures and a Circuit Breaker for preventing cascading failures against a completely down service.
Retry Policies
Built-in Policies
| Policy | Factory | Behaviour |
|---|---|---|
| Exponential Backoff | RetryPolicies.exponential(attempts) | Delay doubles with each attempt plus small jitter — default choice for most cases |
| Fixed Delay | RetryPolicies.fixedDelay(attempts, delayMs) | Constant wait between every retry |
| Linear Backoff | RetryPolicies.linearBackoff(attempts, stepMs) | Delay grows linearly (attempt × stepMs) |
| Full Jitter | RetryPolicies.fullJitter(attempts, baseMs) | Fully random delay within exponential cap — best for spreading concurrent load |
| Decorrelated Jitter | RetryPolicies.decorrelatedJitter(attempts, baseMs, maxDelayMs) | AWS-recommended algorithm with the widest spread across concurrent clients |
All policies retry on 429, 502, 503, 504, and network errors by default.
import { RetryPolicies } from '@yildizpay/http-adapter';
RetryPolicies.exponential(3);
RetryPolicies.fixedDelay(3, 1_000); // 1 s between each attempt
RetryPolicies.linearBackoff(3, 500); // 500 ms → 1000 ms → 1500 ms
RetryPolicies.fullJitter(3, 100); // random within [0, 2^attempt × 100 ms]
RetryPolicies.decorrelatedJitter(3, 100); // AWS decorrelated jitter, cap 30 sCustom Retry Predicate
Override the default retry decision (error.isRetryable()) for any policy via .retryIf(). Accepts an inline function or a class implementing RetryPredicate:
import {
RetryPolicies,
RetryPredicate,
BaseAdapterException,
isNetworkException,
} from '@yildizpay/http-adapter';
// Inline function — only retry on network-level errors
const policy = RetryPolicies.exponential(3)
.retryIf((error) => isNetworkException(error));
// Class-based predicate — combine library signal with your own logic
class BusinessRetryPredicate implements RetryPredicate {
shouldRetry(error: BaseAdapterException): boolean {
return error.isRetryable() && myFeatureFlags.retriesEnabled();
}
}
const policy = RetryPolicies.fullJitter(3, 100)
.retryIf(new BusinessRetryPredicate());Attaching a Retry Policy to the Adapter
const adapter = HttpAdapter.builder()
.withRetryPolicy(RetryPolicies.exponential(3))
.build();Circuit Breaker
The Circuit Breaker protects your system from waiting on a completely down downstream service. After a configured number of consecutive failures, the circuit opens and subsequent requests fail immediately with CircuitBreakerOpenException — without hitting the unresponsive server.
Configuration
import { CircuitBreaker, CircuitBreakerOpenException } from '@yildizpay/http-adapter';
const breaker = new CircuitBreaker({
failureThreshold: 5, // Trip after 5 consecutive failures
resetTimeoutMs: 30_000, // Probe again after 30 seconds
successThreshold: 1, // Close circuit after 1 successful probe
});try {
await adapter.send(request);
} catch (err) {
if (err instanceof CircuitBreakerOpenException) {
console.warn(`Circuit is open. Retry after ${err.retryAfterMs()}ms`);
}
}State Machine
[CLOSED] ──(failureThreshold reached)──▶ [OPEN]
▲ │
│ (resetTimeoutMs)
│ │
└──(successThreshold met)──── [HALF_OPEN] ──(failure)──▶ [OPEN]| State | Behaviour |
|---|---|
CLOSED | Normal operation. Failures are counted. |
OPEN | All requests fail immediately with CircuitBreakerOpenException. |
HALF_OPEN | One probe request is allowed through. Success → CLOSED. Failure → OPEN. |
Why Only One Probe in HALF_OPEN?
Node.js's async/await introduces cooperative multitasking: while one coroutine is awaiting, the event loop can start other coroutines. Without a guard, every request arriving during HALF_OPEN would read the same state and proceed concurrently — potentially overwhelming a service that has only just started to recover.
The Circuit Breaker uses a probe flag: only the first caller gets the probe slot. All concurrent callers in HALF_OPEN receive CircuitBreakerOpenException until the probe resolves. This is a deliberate trade-off — a few extra rejections in exchange for a controlled, safe recovery test.
Attaching to the Adapter
const adapter = HttpAdapter.builder()
.withCircuitBreaker({
failureThreshold: 5,
resetTimeoutMs: 30_000,
successThreshold: 1,
})
.build();Or pass a CircuitBreaker instance directly (required when you also want to attach an observer):
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 })
.observe(new CircuitMetricsObserver());
const adapter = HttpAdapter.builder()
.withCircuitBreaker(breaker)
.build();See Observability for the full CircuitBreakerObserver API.
