Photo by Troy Bridges on Unsplash
What is Circuit Breaker Pattern: How It Enhances Frontend and Backend Functionality
We all are very well aware of the circuit breaker in our houses.
A circuit breaker is a safety device that typically trips (opens the circuit) when there is an electrical fault that could damage the circuit. Excess current, a power surge, or a faulty component usually causes this fault.
➡️ When things work fine, the switch is closed, and electricity flows.
➡️ When there's a problem, the switch opens to stop the electricity flow and prevent damage.
The Circuit Breaker Pattern works the same way in software engineering.
➡️ If a service keeps failing, the circuit breaker opens and stops calls to that service.
➡️ Once the service recovers, the circuit closes again, and calls resume as normal.
There are main two causes why we need to use circuit breaker patterns in our systems:
1) Server Overload
Imagine you have 5 different services, and a web server acts as the messenger to connect users to these services. Whenever someone makes a request, the web server assigns a worker (called a thread) to handle it.
Here’s the problem: One of the services is a bit slow because of an issue. The worker assigned to it just sits and waits. Not a big deal if it’s only one worker.
But what if this slow service is really popular and gets lots of requests? More and more workers will be sent to wait for it. Soon, most of the workers (let’s say 98 out of 100) are stuck waiting for this slow service.
Now, if the other two workers are also handling different tasks, all 100 workers are busy, and no new requests can be handled. The new requests form a long waiting line (a queue). Let’s say 50 more requests come in while the workers are stuck.
A few seconds later, the slow service finally recovers and starts working again. But now, the web server is overwhelmed trying to process all those requests in the queue. Worse, even more requests keep coming in while the server is catching up.
This creates a vicious cycle where the server never gets a break, eventually crashing or becoming useless. And that’s how your service could end up “dying” in this scenario.
When a service becomes slow or unresponsive, the circuit breaker "trips" after detecting multiple failed or slow requests. Instead of sending more workers to that slow service, the circuit breaker stops sending requests to it temporarily.
2) Cascading Failure:
Imagine this chain of events:
Service A asks Service B for something.
Service B then asks Service C.
Service C relies on Service D to finish its task.
But here’s the catch: What happens if one service doesn’t respond on time?
Let’s say Service D is delayed.
Service C is stuck waiting for Service D.
Since Service C can’t finish, Service B is also left waiting.
And because Service B is delayed, Service A is stuck too!
This creates a domino effect where one slow service causes everything in the chain to slow down, leading to a cascade failure.
When one service in a chain (like Service D) becomes unresponsive, the circuit breaker stops the chain early by failing fast. Instead of waiting forever for a response from Service D, the circuit breaker immediately returns an error or a fallback response.
Examples in Action:
🌐 Frontend:
Imagine you’re building a React-based e-commerce app. Users search for products, but during peak traffic (like a sale), the product API starts failing.
Without a circuit breaker:
➡️ The app keeps calling the API repeatedly, worsening server overload.
➡️ Users are stuck with endless spinners or generic error messages.
With a circuit breaker:
➡️ After detecting multiple failures, the circuit opens and blocks further API requests.
➡️ The app displays a friendly fallback message like: "We’re experiencing high traffic. Please try again shortly."
➡️ After a cooldown period, the circuit closes, and the app retries the API.
🛠️ Backend:
In a Node JS microservice, Service A needs data from Service B. If Service B is down:
➡️ The circuit opens to prevent Service A from overloading Service B.
➡️ Meanwhile, Service A can show cached data or default responses to users.
➡️ Once Service B recovers, the circuit closes, and normal calls resume.
Caveats:
🔹 Latency in Detection: It takes time to detect failures, so tune the thresholds wisely.
🔹 Fallbacks Need Thought: What should your app show when the circuit is open? Plan this carefully.
🔹 Overhead: Circuit breakers add complexity, so use them when the benefits outweigh the costs.
The Circuit Breaker Pattern keeps your system reliable and user-friendly, even during failures.
It’s a lifesaver for both frontend and backend apps!
Here is the complete code snippets for the circuit breaker pattern implementation:
interface CircuitBreakerOptions {
failureThreshold?: number; // Max failures before opening the circuit
cooldownPeriod?: number; // Cooldown time in milliseconds
}
type RequestFunction<T> = (...args: any[]) => Promise<T>;
class CircuitBreaker<T> {
private requestFunction: RequestFunction<T>;
private failureThreshold: number;
private cooldownPeriod: number;
private failureCount: number = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private lastFailureTime: number | null = null;
constructor(requestFunction: RequestFunction<T>, options: CircuitBreakerOptions = {}) {
this.requestFunction = requestFunction;
const defaultOptions = {
failureThreshold: 3,
cooldownPeriod: 5000,
};
const finalOptions = { ...defaultOptions, ...options };
this.failureThreshold = finalOptions.failureThreshold;
this.cooldownPeriod = finalOptions.cooldownPeriod;
}
/**
* Makes a request using the circuit breaker.
* @param args - Arguments to pass to the request function.
* @returns A promise resolving with the result of the request function.
* @throws An error if the circuit is OPEN or the request fails.
*/
async call(...args: any[]): Promise<T> {
if (this.state === 'OPEN') {
const now = Date.now();
if (this.lastFailureTime && now - this.lastFailureTime > this.cooldownPeriod) {
console.info('Cooldown period ended. Transitioning to HALF_OPEN state.');
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit is OPEN. Try again later.');
}
}
try {
const result = await this.requestFunction(...args);
this.reset(); // Reset on success
return result;
} catch (error) {
this.recordFailure();
throw error;
}
}
private reset() {
this.state = 'CLOSED';
this.failureCount = 0;
console.info('Circuit state reset to CLOSED.');
}
private open() {
this.state = 'OPEN';
this.lastFailureTime = Date.now();
console.warn('Circuit transitioned to OPEN state.');
}
private recordFailure() {
this.failureCount++;
console.error(`Failure recorded. Count: ${this.failureCount}`);
if (this.failureCount >= this.failureThreshold) {
this.open();
}
}
}
interface RetryOptions {
retries?: number;
initialDelay?: number;
backoffFactor?: number;
}
/**
* Retry a function with customizable exponential backoff.
* @param cb - The callback function to retry.
* @param options - Configuration options for retries.
* @returns A promise resolving with the result of the callback.
*/
const retryWithBackoff = async <T>(
cb: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> => {
const { retries = 3, initialDelay = 1000, backoffFactor = 2 } = options;
let delay = initialDelay;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await cb();
} catch (error) {
if (attempt === retries) throw error;
console.warn(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= backoffFactor; // Apply exponential backoff
}
}
throw new Error('Retries exhausted.');
};
// Example API request function
const fetchProductData: RequestFunction<any> = async () => {
const response = await fetch('https://api.example.com/products');
if (!response.ok) {
throw new Error(`Failed to fetch product data. Status: ${response.status}`);
}
return response.json();
};
// Circuit breaker instance
const circuitBreaker = new CircuitBreaker(fetchProductData, {
failureThreshold: 2,
cooldownPeriod: 5000, // 5 seconds
});
// Main function to fetch data with circuit breaker and retries
const fetchWithCircuitBreaker = async () => {
try {
const data = await retryWithBackoff(
() => circuitBreaker.call(),
{ retries: 3, initialDelay: 1000, backoffFactor: 2 }
);
console.log('Fetched product data:', data);
} catch (error: any) {
console.error('Final error:', error.message);
}
};
// Trigger the function
fetchWithCircuitBreaker();