Building Resilient Applications with Circuit Breaker Pattern in Node.js
Introduction
In today's microservices architecture, applications often depend on multiple external services. When these services fail or become slow, they can cascade failures throughout your entire system. The Circuit Breaker pattern is a crucial design pattern that helps prevent these cascading failures by monitoring external service calls and temporarily disabling them when they become unreliable.
Understanding the Circuit Breaker Pattern
The Circuit Breaker pattern works similar to an electrical circuit breaker in your home. It has three states:
- Closed: Normal operation - requests flow through to the external service
- Open: Service is failing - requests are immediately rejected without calling the service
- Half-Open: Testing state - allows limited requests to test if the service has recovered
This pattern prevents your application from repeatedly calling a failing service, reduces resource consumption, and provides faster failure responses to users.
Implementing a Basic Circuit Breaker
Let's build a simple Circuit Breaker class in Node.js:
class CircuitBreaker {
constructor(request, options = {}) {
this.request = request;
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.nextAttempt = Date.now();
// Configuration options
this.failureThreshold = options.failureThreshold || 5;
this.successThreshold = options.successThreshold || 2;
this.timeout = options.timeout || 60000; // 1 minute
this.retryTimeout = options.retryTimeout || 30000; // 30 seconds
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
this.successCount = 0;
}
try {
const result = await Promise.race([
this.request.apply(this, args),
this.timeoutPromise()
]);
return this.onSuccess(result);
} catch (error) {
return this.onFailure(error);
}
}
onSuccess(result) {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.successThreshold) {
this.state = 'CLOSED';
}
}
return result;
}
onFailure(error) {
this.failureCount++;
if (this.failureCount >= this.failureThreshold || this.state === 'HALF_OPEN') {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.retryTimeout;
}
throw error;
}
timeoutPromise() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'));
}, this.timeout);
});
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
nextAttempt: this.nextAttempt
};
}
}Real-World Usage Example
Here's how you can use the Circuit Breaker with an external API call:
const axios = require('axios');
// Function that calls external service
const callExternalAPI = async (userId) => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return response.data;
};
// Create circuit breaker instance
const apiCircuitBreaker = new CircuitBreaker(callExternalAPI, {
failureThreshold: 3,
successThreshold: 2,
timeout: 5000,
retryTimeout: 20000
});
// Usage in your application
async function getUserData(userId) {
try {
const userData = await apiCircuitBreaker.call(userId);
return userData;
} catch (error) {
console.log('Circuit breaker state:', apiCircuitBreaker.getState());
// Return fallback data or cached response
return {
id: userId,
name: 'Unknown User',
email: 'unavailable@example.com',
cached: true
};
}
}Advanced Features and Monitoring
For production applications, consider adding these enhancements:
Event Emitter for Monitoring
const EventEmitter = require('events');
class EnhancedCircuitBreaker extends CircuitBreaker {
constructor(request, options = {}) {
super(request, options);
this.events = new EventEmitter();
}
onSuccess(result) {
const previousState = this.state;
const newResult = super.onSuccess(result);
if (previousState === 'HALF_OPEN' && this.state === 'CLOSED') {
this.events.emit('circuitClosed');
}
this.events.emit('success');
return newResult;
}
onFailure(error) {
const previousState = this.state;
try {
super.onFailure(error);
} catch (err) {
if (previousState === 'CLOSED' && this.state === 'OPEN') {
this.events.emit('circuitOpened', error);
}
this.events.emit('failure', error);
throw err;
}
}
}Using with Express.js Middleware
const createCircuitBreakerMiddleware = (circuitBreaker) => {
return async (req, res, next) => {
try {
req.circuitBreaker = circuitBreaker;
next();
} catch (error) {
res.status(503).json({
error: 'Service temporarily unavailable',
state: circuitBreaker.getState()
});
}
};
};Best Practices and Considerations
- Fallback Strategies: Always implement fallback mechanisms when the circuit is open
- Monitoring: Log state changes and failures for debugging and alerting
- Configuration: Make thresholds and timeouts configurable per service
- Testing: Test all three states thoroughly in your application
- Graceful Degradation: Provide meaningful responses when services are unavailable
Conclusion
The Circuit Breaker pattern is essential for building resilient distributed systems. By implementing this pattern, you can prevent cascading failures, improve user experience during outages, and make your applications more robust. Start with a simple implementation and gradually add monitoring and advanced features as your system grows.
Remember to tune the thresholds and timeouts based on your specific use cases and monitor the circuit breaker's behavior in production to ensure optimal performance.
Related Posts
Implementing CQRS Pattern in Node.js: Separating Reads from Writes for Better Scalability
Learn how to implement Command Query Responsibility Segregation (CQRS) in Node.js to improve performance and scalability in complex applications.
Building Resilient Microservices with Circuit Breaker Pattern
Learn how to implement the Circuit Breaker pattern to prevent cascading failures and build fault-tolerant microservices architecture.
Building Scalable Microservices with Event-Driven Architecture: A Practical Guide
Learn how to design resilient microservices using event-driven patterns with practical examples and implementation strategies.