Skip to content

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

PolicyFactoryBehaviour
Exponential BackoffRetryPolicies.exponential(attempts)Delay doubles with each attempt plus small jitter — default choice for most cases
Fixed DelayRetryPolicies.fixedDelay(attempts, delayMs)Constant wait between every retry
Linear BackoffRetryPolicies.linearBackoff(attempts, stepMs)Delay grows linearly (attempt × stepMs)
Full JitterRetryPolicies.fullJitter(attempts, baseMs)Fully random delay within exponential cap — best for spreading concurrent load
Decorrelated JitterRetryPolicies.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.

typescript
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 s

Custom Retry Predicate

Override the default retry decision (error.isRetryable()) for any policy via .retryIf(). Accepts an inline function or a class implementing RetryPredicate:

typescript
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

typescript
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

typescript
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
});
typescript
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]
StateBehaviour
CLOSEDNormal operation. Failures are counted.
OPENAll requests fail immediately with CircuitBreakerOpenException.
HALF_OPENOne 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

typescript
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):

typescript
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.

Released under the MIT License.