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:

  1. 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.
  2. Open: The breaker is "tripped." Any further calls to processPayment are immediately rejected with an exception (like CircuitBreakerOpenException or the underlying exception if configured) without even attempting to call the PaymentService. This prevents overwhelming a struggling service and immediately returns a failure to the caller. After a configured waitDurationInOpenState (e.g., 30 seconds), it transitions to the Half-Open state.
  3. 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 the PaymentService has recovered and transitions back to Closed. If any of these test calls fail, it immediately trips back to Open, restarting the waitDurationInOpenState timer.

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.

Want structured learning?

Take the full Distributed Systems course →