The circuit breaker pattern doesn’t just prevent failures; it actively uses failures to improve system resilience.
Let’s watch a client service, OrderService, interact with a PaymentService.
// OrderService client code
public class OrderService {
private final PaymentServiceClient paymentClient;
public OrderService(PaymentServiceClient paymentClient) {
this.paymentClient = paymentClient;
}
public void processOrder(Order order) {
// ... order processing logic ...
try {
PaymentResult result = paymentClient.processPayment(order.getPaymentDetails());
if (result.isSuccess()) {
// ... update order status ...
} else {
// ... handle payment failure ...
}
} catch (PaymentServiceUnavailableException e) {
// This is where the circuit breaker kicks in
System.err.println("Payment service is currently unavailable. Order processing halted.");
// Potentially fallback logic here
}
}
}
Here’s a simplified PaymentServiceClient that might use a circuit breaker library like Resilience4j:
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import java.time.Duration;
public class PaymentServiceClient {
private final CircuitBreaker circuitBreaker;
public PaymentServiceClient() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% of calls must fail to trip the breaker
.waitDurationInOpenState(Duration.ofSeconds(30)) // Stay open for 30 seconds
.permittedNumberOfCallsInHalfOpenState(3) // Allow 3 calls in half-open state
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10) // Consider the last 10 calls
.recordExceptions(
java.net.ConnectException.class,
java.net.SocketTimeoutException.class,
PaymentServiceUnavailableException.class // Custom exception
)
.build();
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
this.circuitBreaker = registry.circuitBreaker("paymentService");
}
public PaymentResult processPayment(PaymentDetails details) {
// Wrap the actual call with the circuit breaker
return CircuitBreaker.decorateSupplier(() -> makeActualPaymentCall(details))
.andTrace(circuitBreaker)
.get();
}
private PaymentResult makeActualPaymentCall(PaymentDetails details) {
// Simulate a call to the actual payment service
System.out.println("Attempting to call Payment Service...");
// In a real scenario, this would be an HTTP call, RPC, etc.
// Simulate a failure for demonstration
if (System.currentTimeMillis() % 4 != 0) { // Fail ~75% of the time for demo
throw new PaymentServiceUnavailableException("Payment gateway is experiencing issues.");
}
return new PaymentResult(true, "Transaction ID: 12345");
}
}
// Dummy classes for demonstration
class Order {}
class PaymentDetails {}
class PaymentResult {
private boolean success;
private String transactionId;
public PaymentResult(boolean success, String transactionId) {
this.success = success;
this.transactionId = transactionId;
}
public boolean isSuccess() { return success; }
public String getTransactionId() { return transactionId; }
}
class PaymentServiceUnavailableException extends RuntimeException {
public PaymentServiceUnavailableException(String message) {
super(message);
}
}
When OrderService calls paymentClient.processPayment, the PaymentServiceClient doesn’t directly execute makeActualPaymentCall. Instead, it delegates to the circuitBreaker. The circuitBreaker watches the outcomes: successful calls, failures, and timeouts.
The circuit breaker has three states:
- Closed: Everything is normal. Calls are passed through to the
PaymentService. The breaker counts failures. If the failure rate exceeds the configured threshold (e.g., 50%), it trips to the Open state. - Open: The breaker is "tripped." Any further calls to
processPaymentare immediately rejected with an exception (likeCircuitBreakerOpenExceptionor the underlying exception if configured) without even attempting to call thePaymentService. This prevents overwhelming a struggling service and immediately returns a failure to the caller. After a configuredwaitDurationInOpenState(e.g., 30 seconds), it transitions to the Half-Open state. - Half-Open: The breaker allows a limited number of test calls (e.g., 3) to pass through to the
PaymentService. If these calls succeed, the breaker assumes thePaymentServicehas recovered and transitions back to Closed. If any of these test calls fail, it immediately trips back to Open, restarting thewaitDurationInOpenStatetimer.
This cycle is how it stops cascading failures. If PaymentService starts failing, OrderService (and any other service calling it) will quickly hit the circuit breaker. The breaker will open, preventing further load on the broken PaymentService. This gives PaymentService time to recover without being bombarded by requests. Meanwhile, OrderService receives immediate failures, allowing it to execute fallback logic (e.g., "try again later," "use a different payment method," or simply inform the user) instead of hanging indefinitely or retrying endlessly.
The most surprising aspect of circuit breakers is their active role in failure detection and recovery. They don’t just react; they govern the flow of traffic based on observed system health, proactively isolating failing components to protect the entire ecosystem. The recordExceptions configuration is crucial here; it tells the breaker which exceptions signify a problem with the downstream service that should contribute to tripping the breaker. If you don’t record the right exceptions, the breaker will never know the service is actually down.
The next challenge you’ll face is implementing robust fallback mechanisms when the circuit breaker is open, ensuring your application remains functional even when critical dependencies are unavailable.