Modern applications rely heavily on external APIs and services, and any failure in these dependencies can lead to cascading failures in your system. To mitigate these risks, implementing circuit breakers and retry strategies becomes crucial. This chapter explores these resilience mechanisms in detail, providing an A-to-Z understanding with practical examples using Express.js.
Circuit breakers are a design pattern that prevents repeated failures by stopping requests to a failing service and allowing recovery time. This avoids overwhelming the failing service and helps maintain system stability.
Retry strategies involve attempting a failed operation multiple times before giving up. These strategies increase the chances of recovery in transient failure scenarios, such as temporary network outages.
A circuit breaker acts like an electrical fuse that “trips” to stop the flow of electricity when there’s a fault. Similarly, in software systems, it “opens” to block requests to a failing service.
Here’s an example of a simple manual circuit breaker:
let failureCount = 0;
let circuitOpen = false;
const FAILURE_THRESHOLD = 5;
const RESET_TIMEOUT = 10000; // 10 seconds
function circuitBreakerMiddleware(req, res, next) {
if (circuitOpen) {
return res.status(503).send("Service Unavailable");
}
next();
}
function handleServiceResponse(success) {
if (!success) {
failureCount++;
if (failureCount >= FAILURE_THRESHOLD) {
circuitOpen = true;
setTimeout(() => {
circuitOpen = false;
failureCount = 0;
}, RESET_TIMEOUT);
}
} else {
failureCount = 0; // Reset on success
}
}
Usage in an Express route:
app.use(circuitBreakerMiddleware);
app.get('/external-service', async (req, res) => {
try {
const response = await someExternalServiceCall();
handleServiceResponse(true);
res.send(response.data);
} catch (error) {
handleServiceResponse(false);
res.status(500).send("Service failed");
}
});
opossum
)
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 10000,
};
const breaker = new CircuitBreaker(someExternalServiceCall, options);
app.get('/external-service', async (req, res) => {
try {
const result = await breaker.fire();
res.send(result);
} catch (error) {
res.status(503).send("Service Unavailable");
}
});
Retry strategies aim to handle transient failures by retrying the operation after a delay. This increases the chances of a successful response.
async function retryOperation(operation, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await operation();
} catch (error) {
if (i < retries - 1) await new Promise((res) => setTimeout(res, delay));
}
}
throw new Error("Operation failed after retries");
}
app.get('/retry-example', async (req, res) => {
try {
const data = await retryOperation(someExternalServiceCall, 5, 2000);
res.send(data);
} catch (error) {
res.status(500).send("Operation failed");
}
});
Using Libraries (axios-retry
)
const axios = require('axios');
const axiosRetry = require('axios-retry');
axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay });
app.get('/retry-example', async (req, res) => {
try {
const response = await axios.get('https://external-service.com/data');
res.send(response.data);
} catch (error) {
res.status(500).send("Service failed");
}
});
Combining both ensures that retries are attempted only when the circuit is not open.
const CircuitBreaker = require('opossum');
const axios = require('axios');
const axiosRetry = require('axios-retry');
axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay });
const breaker = new CircuitBreaker(() => axios.get('https://external-service.com/data'), {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 10000,
});
app.get('/resilient-endpoint', async (req, res) => {
try {
const response = await breaker.fire();
res.send(response.data);
} catch (error) {
res.status(503).send("Service Unavailable");
}
});
Use monitoring tools like Prometheus or New Relic to track circuit breaker states and retry attempts. Implement structured logging using libraries like winston.
Circuit breakers and retry strategies are essential for building resilient applications that can handle failures gracefully. This chapter has provided an in-depth guide to implementing these patterns in Express.js, ensuring you can create reliable and robust systems. Happy coding !❤️