Circuit breakers don’t just trip; they actively participate in load balancing by selectively shedding load to prevent catastrophic failures.
Let’s watch a circuit breaker in action. Imagine a simple web service, UserService, that depends on an external EmailService. We’ll use a Java application with Resilience4j for our circuit breaker.
Here’s the UserService code:
@Service
public class UserService {
private final EmailService emailService;
private final CircuitBreaker circuitBreaker;
public UserService(EmailService emailService) {
this.emailService = emailService;
this.circuitBreaker = CircuitBreaker.ofDefaults("emailServiceCircuitBreaker");
}
public String registerUser(String username) {
// Decorate the call to emailService.sendWelcomeEmail with the circuit breaker
Supplier<String> emailCall = () -> emailService.sendWelcomeEmail(username);
String result = circuitBreaker.executeSupplier(emailCall);
return "User " + username + " registered. Email status: " + result;
}
}
And the EmailService (which will simulate failures):
@Service
public class EmailService {
private int requestCount = 0;
public String sendWelcomeEmail(String username) {
requestCount++;
if (requestCount % 3 == 0) { // Simulate failure every 3rd call
throw new RuntimeException("Email service is overloaded!");
}
return "Welcome email sent to " + username;
}
}
We configure Resilience4j’s circuit breaker in application.yml:
resilience4j.circuitbreaker:
instances:
emailServiceCircuitBreaker:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 10 # Consider the last 10 calls
minimumNumberOfCalls: 5 # At least 5 calls before considering a trip
permittedNumberOfCallsInHalfOpenState: 3 # Allow 3 calls in half-open
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s # Wait 5 seconds before moving to half-open
recordExceptions: # Exceptions that count towards tripping
- java.lang.RuntimeException
Now, let’s simulate load testing. We’ll hit the registerUser endpoint repeatedly.
- First few calls:
UserServicecallsEmailService.EmailServicesucceeds. Circuit breaker records successes. - Third call:
EmailServicethrowsRuntimeException. Circuit breaker records a failure. - Fourth, Fifth calls:
EmailServicesucceeds again. Circuit breaker still sees a low failure rate. - Sixth call:
EmailServicethrowsRuntimeException. The failure rate now crosses the threshold (e.g., if 3 out of 6 calls failed, and our threshold is 50%, it trips). The circuit breaker transitions toOPEN. - Subsequent calls (while OPEN):
UserServiceno longer callsEmailService. ThecircuitBreaker.executeSupplier(emailCall)immediately returns a fallback (or throws aCallNotPermittedException). TheUserServicereturns "User X registered. Email status: Circuit breaker is open." This protectsEmailServicefrom further load. - After
waitDurationInOpenState(5s): The circuit breaker automatically transitions toHALF-OPEN. - First call in HALF-OPEN:
UserServicecallsEmailService. If it succeeds, the circuit breaker transitions back toCLOSED. If it fails, it goes back toOPENfor another 5 seconds. - Subsequent calls in HALF-OPEN: If the circuit breaker is
HALF-OPEN, it allows a limited number of calls (permittedNumberOfCallsInHalfOpenState). If these succeed, it closes. If any fail, it opens again.
This dance of CLOSED, OPEN, and HALF-OPEN states, driven by call statistics and configured thresholds, is the core of circuit breaker behavior.
The most surprising thing about circuit breakers is that they don’t just react to failures; they anticipate them based on past behavior and actively prevent them by short-circuiting. The HALF-OPEN state is crucial for recovery, acting as a canary test to see if the downstream service has healed before fully re-engaging it.
The next step is understanding how to integrate different fallback mechanisms when the circuit breaker is open, beyond just returning an error.