DEV Community

Fernanda Nadhiftya
Fernanda Nadhiftya

Posted on

The Circuit Breaker Pattern: Building Resilient Applications

Imagine you're at home and a power surge threatens to fry your appliances. What saves them? A circuit breaker - a simple device that cuts the power when it detects danger, protecting everything downstream from damage.

In software engineering, we face a similar problem. When a service your application depends on starts failing, those failures can cascade through your system, bringing everything down. The Circuit Breaker design pattern applies the same protective principle to software - it "trips" when failures are detected, preventing your application from repeatedly trying to execute an operation that's likely to fail.

The Problem: Cascading Failures

Consider a typical microservices architecture where Service A calls Service B, which calls Service C. What happens when Service C goes down?

Without protection,

  1. Service C fails to respond
  2. Service B waits for the timeout, holding resources
  3. Service A's requests pile up waiting for Service B
  4. Users experience slow responses or complete failures
  5. Eventually, the entire system grinds to a halt

This is a cascading failure - and it's one of the most common ways distributed systems fail catastrophically.

The Solution: Circuit Breaker Pattern

The Circuit Breaker pattern, popularized by Michael Nygard in his book Release It!, provides a elegant solution. Like its electrical namesake, it monitors for failures and "trips" when a threshold is reached, immediately failing subsequent requests instead of waiting for inevitable timeouts.

The Three States

A circuit breaker operates in three distinct states:

1. Closed State (Normal Operation)

The circuit is closed, and requests flow through normally. The breaker monitors for failures.

2. Open State (Failure Mode)

When failures exceed a threshold, the circuit "trips" open. All requests immediately fail without attempting the operation - this is called fail-fast behavior.

3. Half-Open State (Recovery Testing)

After a timeout period, the breaker allows a single test request through. If it succeeds, the circuit closes. If it fails, the circuit opens again.

Implementation Deep Dive

Let's examine a real-world TypeScript implementation of the Circuit Breaker pattern using the State design pattern:

The State Interface

Each state implements this interface, encapsulating state-specific behavior. This is a clean application of the State pattern - behavior changes based on internal state without complex conditionals.

export interface CircuitBreakerState {
    name: string;
    call<T>(action: () => Promise<T>): Promise<T>;
}
Enter fullscreen mode Exit fullscreen mode

The Closed State

In the Closed state, every call passes through to the actual operation. Successes reset the failure count; failures increment it. When the failure threshold is reached, the breaker transitions to the Open state.

class CircuitBreakerClosedState implements CircuitBreakerState {
    private circuitBreaker: CircuitBreaker;

    constructor(circuitBreaker: CircuitBreaker) {
        this.circuitBreaker = circuitBreaker;
    }

    async call<T>(action: () => Promise<T>): Promise<T> {
        try {
            const result = await action();
            this.circuitBreaker.recordSuccess();
            return result;
        } catch (error) {
            this.circuitBreaker.recordFailure();
            throw error;
        }
    }

    get name(): string {
        return "CLOSED";
    }
}
Enter fullscreen mode Exit fullscreen mode

The Open State

The Open state is pretty simple - it rejects all calls immediately. No waiting for timeouts, no consuming resources. This fail-fast behavior is the key to preventing cascading failures.

class CircuitBreakerOpenState implements CircuitBreakerState {
    async call<T>(action: () => Promise<T>): Promise<T> {
        throw new Error("Circuit breaker is OPEN. Calls are not allowed.");
    }

    get name(): string {
        return "OPEN";
    }
}
Enter fullscreen mode Exit fullscreen mode

The Half-Open State

The Half-Open state is the "test the waters" phase. A single request is allowed through:

  • If the request succeeds, the circuit closes, normal operation resumes
  • If the request fails, the circuit opens again, extending the protection period

The Circuit Breaker Core

export class CircuitBreaker {
    private failureThreshold: number;
    private recoveryTimeout: number;
    private _failureCount: number;
    private _state: CircuitBreakerState;
    private timeoutHandler: NodeJS.Timeout | null = null;

    constructor(options?: CircuitBreakerOptions) {
        this._failureCount = 0;
        this.failureThreshold = options?.failureThreshold ?? 5;
        this.recoveryTimeout = options?.recoveryTimeout ?? 10000;
        this._state = new CircuitBreakerClosedState(this);
    }

    // ... state transition methods
}
Enter fullscreen mode Exit fullscreen mode

Circuit Breaker in Action

Here's a circuit breaker implementation for handling consequent failing calls to a CAS's serviceValidate endpoint:

// src/contexts/AuthContext.tsx

import { CircuitBreaker } from "@/lib/circuit-breaker";
import React, { useCallback, useRef } from "react";

// ... more imports

export function AuthProvider({
    allowedHost,
    children,
}: Readonly<{
    allowedHost: string;
    children: React.ReactNode;
}>) {
  // ... more hooks
  const loginCircuitBreakerRef = useRef(
      new CircuitBreaker({ failureThreshold: 3, recoveryTimeout: 30000 }),
  );

  // ... more code

  const login = useCallback(
      async (ticket: string) => {
          // ... more code here

          let loginData;
          try {
              const validateUrl = new URL(
                  "/api/auth/validate",
                  window.location.origin,
              );
              validateUrl.searchParams.set("ticket", ticket);

              // Leverage circuit breaker pattern to prevent
              // sending consequent requests to a failing service
              loginData = await loginCircuitBreakerRef.current.call(
                  async () => {
                      const loginRes = await createLoginResponse(validateUrl);
                      return await parseLoginResponse(loginRes);
                  },
              );
          } catch (err) {
              // ... error handler here
          } finally {
              loadingRef.current = false;
          }

          // ... set new login data here

      },
      [createLoginResponse, parseLoginResponse, router],
  );

  // ... more code

}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The Circuit Breaker pattern is essential for building resilient distributed systems. By failing fast when a dependency is struggling, we prevent cascading failures from happening, preserve resources for requests that can succeed, and allow recovery time for struggling services.

Top comments (0)