In a distributed environment, resilience and fault tolerance are important. Two essential techniques, the Retry Pattern and the Circuit Breaker Pattern, stand out as pillars in the search for robustness.
Both patterns appear to address identical issues like improving system reliability by gracefully accepting temporary errors. But when you dive in deeper, you'll figure out there are some small but important differences in their techniques, trade-offs, and ideal use cases.
In this post, we will compare the Retry Pattern with the Circuit Breaker Pattern to discover their features, advantages and disadvantages.
Let’s jump into the differences.
The Retry Pattern enables an application to handle transient failures when it tries to connect to a service or network resource by transparently retrying a failed operation.
The Circuit Breaker Pattern is designed to stop making requests to a service or component that's not working properly. It does this by temporarily 'breaking' the connection and sending requests to an alternate path or giving a backup response instead. The Retry Pattern enables an application to handle transient failures when it tries to connect to a service or network resource by transparently retrying a failed operation.
In the Retry Pattern, retries are attempted immediately or with a slight delay after each failure, without considering the nature or cause of the failure.
The Circuit Breaker keeps track of the number of successive failures and opens the circuit when a predefined threshold is reached. It stays open for a predetermined amount of time, during which requests are not passed along, giving the failed component time to recover.
The Retry pattern provides a basic way to handle fault tolerance by making multiple attempts to overcome transient failures. However, if the failure persists or gets worse then making continuous attempts might make things worse.
The Circuit Breaker provides a more advanced way to handle fault tolerance by actively controlling the flow of requests. It prevents cascading failures by quickly isolating problematic components and failing fast, thus minimizing the impact on the overall system.
The retry pattern may increase resource consumption, particularly when they happen quickly or without backup plans. Each retry attempt consumes additional resources.
The Circuit Breaker Pattern conserves resources by cutting down unnecessary calls to failing services or components. Once the circuit is open, requests are diverted, relieving the burden on the failing resource.
The Retry Pattern mostly depends on repetitive attempts for recovery without having adaptive behavior. It might not fit effectively to long-lasting or systemic failures.
The Circuit Breaker Pattern enables the system to respond dynamically based on changing circumstances, which encourage adaptive behavior. It can gradually close the circuit as the failing component recovers or divert traffic to alternative resources.
The Retry Pattern is easier to implement and it requires less configuration and code modifications. It works well for scenarios where transient failures are common and immediate retries are acceptable.
The circuit breaker Pattern uses more complex logic to achieve state management to track the state of circuits and handle transitions between states. It's preferred in scenarios when fault tolerance and system stability are crucial.
The Retry Pattern handles the failure using below strategies:
Cancel.
Retry.
Retry after delay.
The Circuit Breaker Pattern handles the failure using below strategies:
Closed.
Open.
Half-Open.
Here is the basic implementation for both patterns so you can get a better understanding.
const retryCount = 3;
const delayInMilliseconds = 2000
export class RetryPattern {
async fetchWithRetry(url: string) {
let currentRetry = 0;
while (currentRetry < retryCount) {
try {
const response = await fetch(url);
// Check if response is successful, if yes, return the data
if (response.ok) {
const data = await response.json();
return data;
} else {
// If response is not successful, throw an error
throw new Error(`Failed to fetch data: ${response.status} - ${response.statusText}`);
}
} catch (error: any) {
// Log the error or handle it as needed
console.error(`Error fetching data: ${error.message}`);
// Increment retries
currentRetry++;
// Wait for a short period before retrying
await new Promise(resolve => setTimeout(resolve, delayInMilliseconds));
}
}
}
}
const FAILURE_THRESHOLD: number = 3;
const DELAY_IN_MILLISECONDS: number = 5000;
enum CircuitState {
Closed,
Open,
HalfOpen
}
class CircuitBreakerPattern {
private failures: number;
private circuitState: CircuitState;
constructor() {
this.failures = 0;
this.circuitState = CircuitState.Closed;
}
async performOperation(operation: () => Promise<any>): Promise<any> {
if (this.circuitState === CircuitState.Open) {
throw new Error('Circuit is open');
}
try {
const result = await operation();
this.reset();
return result;
} catch (error) {
this.failures++;
if (this.failures >= FAILURE_THRESHOLD) {
this.openCircuit();
}
throw error;
}
}
private openCircuit(): void {
this.circuitState = CircuitState.Open;
setTimeout(() => {
this.circuitState = CircuitState.HalfOpen;
}, DELAY_IN_MILLISECONDS);
}
private reset(): void {
this.failures = 0;
this.circuitState = CircuitState.Closed;
}
}
Thanks for reading!
If you like the content, please do not forget to subscribe the GetDifferences channel.