DEV Community

Cover image for 5 Essential Circuit Breaking Patterns for Java Microservices Resilience
Aarav Joshi
Aarav Joshi

Posted on

5 Essential Circuit Breaking Patterns for Java Microservices Resilience

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

I've studied circuit breaking patterns in Java extensively, analyzing how they create resilient distributed systems. Circuit breakers serve as critical safeguards that protect applications from cascading failures when components degrade or fail. Let me share what I've learned about the five essential patterns.

Circuit Breaking Fundamentals

Circuit breakers monitor remote calls and trip when failure thresholds are exceeded. Like electrical circuit breakers, they prevent system overload by stopping additional calls to failing services.

In distributed Java applications, this pattern is vital. When microservices interact, failures in one service can rapidly spread throughout the system. Circuit breakers create isolation points that contain these failures.

The circuit breaker state machine follows three states: closed (normal operation), open (calls are blocked), and half-open (testing if the service has recovered). This state management is the foundation of resilient system design.

public class SimpleCircuitBreaker {
    private State state;
    private int failureCount;
    private int failureThreshold;
    private long openTimestamp;
    private long resetTimeout;

    public SimpleCircuitBreaker(int failureThreshold, long resetTimeout) {
        this.state = State.CLOSED;
        this.failureCount = 0;
        this.failureThreshold = failureThreshold;
        this.resetTimeout = resetTimeout;
    }

    public synchronized <T> T execute(Supplier<T> supplier, Supplier<T> fallback) {
        if (state == State.OPEN) {
            if (System.currentTimeMillis() - openTimestamp > resetTimeout) {
                state = State.HALF_OPEN;
            } else {
                return fallback.get();
            }
        }

        try {
            T result = supplier.get();
            if (state == State.HALF_OPEN) {
                reset();
            }
            return result;
        } catch (Exception e) {
            recordFailure();
            return fallback.get();
        }
    }

    private void recordFailure() {
        failureCount++;
        if (failureCount >= failureThreshold || state == State.HALF_OPEN) {
            state = State.OPEN;
            openTimestamp = System.currentTimeMillis();
        }
    }

    private void reset() {
        state = State.CLOSED;
        failureCount = 0;
    }

    private enum State {
        CLOSED, OPEN, HALF_OPEN
    }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 1: Timeout-Based Circuit Breaking

Timeouts are the first line of defense against unresponsive services. Without proper timeout configuration, applications can exhaust thread pools and connection resources while waiting for responses that may never arrive.

I've found that effective timeout strategies require a multi-layered approach. Connection timeouts prevent hanging when services are completely down, while read timeouts limit waiting for slow responses.

Modern Java HTTP clients like OkHttp and HttpClient support timeout configuration out of the box:

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/resource"))
    .timeout(Duration.ofSeconds(10))
    .build();

try {
    HttpResponse<String> response = client.send(request, 
        HttpResponse.BodyHandlers.ofString());
    return response.body();
} catch (HttpTimeoutException e) {
    logger.warn("Request timed out", e);
    return fallbackResponse();
}
Enter fullscreen mode Exit fullscreen mode

When working with service discovery and load balancers, add an extra layer of timeout protection:

RetryableClient retryableClient = RetryableClient.builder()
    .withConnectTimeout(3, TimeUnit.SECONDS)
    .withReadTimeout(5, TimeUnit.SECONDS)
    .withRetryPolicy(new ExponentialBackoffRetry(3, 1000, 10000))
    .build();
Enter fullscreen mode Exit fullscreen mode

Database operations also need timeout protection. I've seen many applications become unresponsive due to long-running queries:

// Using Hibernate/JPA
properties.put("javax.persistence.query.timeout", "5000");
properties.put("hibernate.jdbc.timeout", "5000");

// Direct JDBC configuration
connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 5000);
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Fallback Strategies

Fallbacks are how applications gracefully handle circuit open states. Without fallbacks, circuit breakers simply convert failures into different failures.

I've implemented several fallback patterns that maintain acceptable user experiences:

Cached responses store previous successful results to use when services are unavailable:

public class CachingFallbackService {
    private final ExternalService service;
    private final Cache<String, ServiceResponse> responseCache;
    private final CircuitBreaker circuitBreaker;

    public CachingFallbackService(ExternalService service) {
        this.service = service;
        this.responseCache = Caffeine.newBuilder()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .build();
        this.circuitBreaker = CircuitBreaker.of("externalService", 
            CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(30))
                .build());
    }

    public ServiceResponse getResource(String resourceId) {
        return Try.ofSupplier(
            CircuitBreaker.decorateSupplier(circuitBreaker, 
                () -> service.fetchResource(resourceId)))
            .recover(throwable -> {
                logger.warn("Using cached response for {}", resourceId);
                return responseCache.getIfPresent(resourceId);
            })
            .getOrElse(() -> getDefaultResponse(resourceId));
    }

    private ServiceResponse getDefaultResponse(String resourceId) {
        return new ServiceResponse(resourceId, Collections.emptyList(), "Default");
    }
}
Enter fullscreen mode Exit fullscreen mode

Degraded functionality delivers core features while disabling enhanced capabilities:

public ProductDetails getProductDetails(String productId) {
    ProductBasic basic = productBasicService.getProduct(productId);

    // Get enhanced details with circuit breaker
    Try<ProductEnhancements> enhancementsTry = Try.of(() -> 
        enhancementCircuitBreaker.executeSupplier(() -> 
            enhancementService.getEnhancements(productId)));

    // Fallback to basic product without enhancements
    ProductEnhancements enhancements = enhancementsTry
        .getOrElse(ProductEnhancements.empty());

    return new ProductDetails(basic, enhancements);
}
Enter fullscreen mode Exit fullscreen mode

Alternative service paths route requests to backup services when primary ones fail:

public UserProfile getUserProfile(String userId) {
    return primaryProfileCircuitBreaker
        .executeSupplier(() -> primaryProfileService.getProfile(userId))
        .recover(Exception.class, ex -> {
            metrics.incrementCounter("primary_profile_fallback");
            return backupProfileCircuitBreaker
                .executeSupplier(() -> backupProfileService.getProfile(userId));
        })
        .recover(Exception.class, ex -> {
            metrics.incrementCounter("profile_default_fallback");
            return UserProfile.createDefault(userId);
        });
}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Half-Open State Monitoring

Automatic recovery is essential for self-healing systems. The half-open state acts as a testing mechanism that determines if failing services have recovered.

Implementing effective half-open state behavior requires careful consideration of test frequency and volume:

public class AdaptiveCircuitBreaker {
    private final String name;
    private final int failureThreshold;
    private final long resetTimeoutMs;
    private final int halfOpenMaxCalls;

    private State state = State.CLOSED;
    private int failures = 0;
    private long openTime = 0;
    private int halfOpenSuccesses = 0;
    private int halfOpenFailures = 0;
    private final AtomicInteger halfOpenConcurrentCalls = new AtomicInteger(0);

    public <T> T execute(Supplier<T> action, Function<Exception, T> fallback) {
        switch (state) {
            case CLOSED:
                return handleClosedState(action, fallback);
            case OPEN:
                return handleOpenState(action, fallback);
            case HALF_OPEN:
                return handleHalfOpenState(action, fallback);
            default:
                throw new IllegalStateException("Unknown circuit state");
        }
    }

    private <T> T handleHalfOpenState(Supplier<T> action, Function<Exception, T> fallback) {
        if (halfOpenConcurrentCalls.incrementAndGet() <= halfOpenMaxCalls) {
            try {
                T result = action.get();
                synchronized (this) {
                    halfOpenSuccesses++;
                    if (halfOpenSuccesses >= 5) { // Configurable success threshold
                        transitionToClosed();
                    }
                }
                halfOpenConcurrentCalls.decrementAndGet();
                return result;
            } catch (Exception e) {
                synchronized (this) {
                    halfOpenFailures++;
                    if (halfOpenFailures >= 2) { // Quick re-open on failures
                        transitionToOpen();
                    }
                }
                halfOpenConcurrentCalls.decrementAndGet();
                return fallback.apply(e);
            }
        } else {
            halfOpenConcurrentCalls.decrementAndGet();
            return fallback.apply(new CircuitBreakerException("Circuit in half-open state"));
        }
    }

    // Other methods omitted for brevity

    private enum State {
        CLOSED, OPEN, HALF_OPEN
    }
}
Enter fullscreen mode Exit fullscreen mode

Progressive recovery tests are more effective than all-or-nothing approaches. Gradually increasing the number of test requests prevents overwhelming recovering services:

public class ProgressiveRecoveryCircuitBreaker extends CircuitBreaker {
    private AtomicInteger halfOpenTestCounter = new AtomicInteger(0);
    private final int initialTestBatch = 5;
    private final double testBatchMultiplier = 2.0;
    private volatile int currentTestBatchSize = initialTestBatch;

    @Override
    protected void afterHalfOpenStateTransition() {
        halfOpenTestCounter.set(0);
        currentTestBatchSize = initialTestBatch;
    }

    @Override
    protected boolean shouldAllowHalfOpenCall() {
        return halfOpenTestCounter.incrementAndGet() <= currentTestBatchSize;
    }

    @Override
    protected void onHalfOpenSuccess() {
        if (halfOpenTestCounter.get() >= currentTestBatchSize) {
            // Expand test batch size for next round
            currentTestBatchSize = (int)(currentTestBatchSize * testBatchMultiplier);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Bulkhead Patterns

Bulkheads contain failures by limiting concurrent requests to downstream services. This isolation prevents resource exhaustion and helps maintain responsiveness.

Thread pool isolation creates separate thread pools for critical services:

public class BulkheadService {
    private final Map<String, ThreadPoolExecutor> servicePools = new ConcurrentHashMap<>();

    public BulkheadService() {
        // Create isolated thread pools for each critical service
        servicePools.put("userService", createBoundedPool(20, "user-service-pool"));
        servicePools.put("paymentService", createBoundedPool(10, "payment-service-pool"));
        servicePools.put("inventoryService", createBoundedPool(15, "inventory-service-pool"));
        // Default pool for less critical services
        servicePools.put("default", createBoundedPool(50, "default-service-pool"));
    }

    private ThreadPoolExecutor createBoundedPool(int maxThreads, String name) {
        return new ThreadPoolExecutor(
            maxThreads / 2,  // Core pool size
            maxThreads,      // Maximum pool size
            60, TimeUnit.SECONDS, // Keep alive time
            new LinkedBlockingQueue<>(100), // Bounded queue to prevent memory issues
            new ThreadFactoryBuilder().setNameFormat(name + "-%d").build(),
            new ThreadPoolExecutor.CallerRunsPolicy() // Back pressure strategy
        );
    }

    public <T> CompletableFuture<T> executeWithBulkhead(String serviceName, Supplier<T> task) {
        ThreadPoolExecutor executor = servicePools.getOrDefault(
            serviceName, servicePools.get("default"));

        return CompletableFuture.supplyAsync(task, executor);
    }
}
Enter fullscreen mode Exit fullscreen mode

Semaphore isolation limits concurrent calls without separate thread pools:

public class SemaphoreBulkhead {
    private final Map<String, Semaphore> serviceLimiters = new ConcurrentHashMap<>();

    public SemaphoreBulkhead() {
        serviceLimiters.put("criticalService", new Semaphore(10));
        serviceLimiters.put("nonCriticalService", new Semaphore(30));
    }

    public <T> T executeWithBulkhead(String serviceName, Supplier<T> task, Supplier<T> fallback) {
        Semaphore semaphore = serviceLimiters.get(serviceName);
        if (semaphore == null) {
            return task.get(); // No limits for this service
        }

        boolean acquired = false;
        try {
            acquired = semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
            if (acquired) {
                return task.get();
            } else {
                return fallback.get();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return fallback.get();
        } finally {
            if (acquired) {
                semaphore.release();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Client-side load balancing distributes requests across multiple instances:

public class LoadBalancedClientCircuitBreaker {
    private final List<ServiceNode> serviceNodes;
    private final Map<ServiceNode, CircuitBreaker> nodeCircuitBreakers;
    private final Random random = new Random();

    public LoadBalancedClientCircuitBreaker(List<ServiceNode> nodes) {
        this.serviceNodes = new ArrayList<>(nodes);
        this.nodeCircuitBreakers = nodes.stream()
            .collect(Collectors.toMap(
                node -> node,
                node -> CircuitBreaker.builder()
                    .name("cb-" + node.getId())
                    .failureRateThreshold(50)
                    .build()
            ));
    }

    public <T> T executeRequest(Function<ServiceNode, T> request, T fallback) {
        // Shuffle available nodes that are not open
        List<ServiceNode> availableNodes = serviceNodes.stream()
            .filter(node -> !nodeCircuitBreakers.get(node).isOpen())
            .collect(Collectors.toList());

        if (availableNodes.isEmpty()) {
            return fallback;
        }

        // Try nodes in random order
        Collections.shuffle(availableNodes);
        for (ServiceNode node : availableNodes) {
            CircuitBreaker cb = nodeCircuitBreakers.get(node);
            try {
                return cb.executeSupplier(() -> request.apply(node));
            } catch (Exception e) {
                // Try next node
                continue;
            }
        }

        return fallback;
    }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Dynamic Circuit Configuration

Adaptive circuit breakers adjust their behavior based on runtime conditions. This dynamic configuration enables systems to respond to changing environments.

Runtime property adjustment allows fine-tuning without application restarts:

public class DynamicCircuitBreakerConfig {
    private final Map<String, CircuitBreakerConfig> configs = new ConcurrentHashMap<>();
    private final ConfigSource configSource;
    private final ScheduledExecutorService configRefresher;

    public DynamicCircuitBreakerConfig(ConfigSource configSource) {
        this.configSource = configSource;
        this.configRefresher = Executors.newSingleThreadScheduledExecutor();

        // Initialize all circuit breaker configs
        refreshConfigs();

        // Schedule periodic config refresh
        configRefresher.scheduleAtFixedRate(
            this::refreshConfigs, 1, 1, TimeUnit.MINUTES);
    }

    private void refreshConfigs() {
        try {
            Map<String, CircuitBreakerProperties> properties = 
                configSource.getCircuitBreakerProperties();

            for (Map.Entry<String, CircuitBreakerProperties> entry : properties.entrySet()) {
                String name = entry.getKey();
                CircuitBreakerProperties props = entry.getValue();

                CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                    .failureRateThreshold(props.getFailureThreshold())
                    .waitDurationInOpenState(Duration.ofMillis(props.getWaitDurationMs()))
                    .slidingWindowType(props.isCountBased() ? 
                        SlidingWindowType.COUNT_BASED : SlidingWindowType.TIME_BASED)
                    .slidingWindowSize(props.getWindowSize())
                    .build();

                configs.put(name, config);
            }
        } catch (Exception e) {
            logger.error("Failed to refresh circuit breaker configs", e);
        }
    }

    public CircuitBreakerConfig getConfig(String name) {
        return configs.get(name);
    }
}
Enter fullscreen mode Exit fullscreen mode

Service health awareness adjusts thresholds based on the health of dependent services:

public class HealthAwareCircuitBreaker {
    private final CircuitBreaker delegate;
    private final HealthIndicator healthIndicator;
    private final ScheduledExecutorService healthChecker;

    private volatile int failureThreshold;
    private volatile int halfOpenSuccessThreshold;

    public HealthAwareCircuitBreaker(String name, HealthIndicator healthIndicator) {
        this.healthIndicator = healthIndicator;
        this.failureThreshold = 50; // Default values
        this.halfOpenSuccessThreshold = 5;

        // Create the delegate with initial settings
        this.delegate = CircuitBreaker.builder()
            .name(name)
            .failureRateThreshold(failureThreshold)
            .build();

        // Start health monitoring
        this.healthChecker = Executors.newSingleThreadScheduledExecutor();
        this.healthChecker.scheduleAtFixedRate(
            this::adjustBasedOnHealth, 30, 30, TimeUnit.SECONDS);
    }

    private void adjustBasedOnHealth() {
        try {
            Health health = healthIndicator.health();
            Status status = health.getStatus();

            if (Status.UP.equals(status)) {
                // Service is healthy - relax circuit breaker
                if (failureThreshold < 50) {
                    failureThreshold = Math.min(failureThreshold + 5, 50);
                    updateDelegateConfig();
                }
                if (halfOpenSuccessThreshold > 2) {
                    halfOpenSuccessThreshold = Math.max(halfOpenSuccessThreshold - 1, 2);
                    updateDelegateConfig();
                }
            } else if (Status.DOWN.equals(status)) {
                // Service is down - tighten circuit breaker
                if (failureThreshold > 10) {
                    failureThreshold = Math.max(failureThreshold - 10, 10);
                    updateDelegateConfig();
                }
                if (halfOpenSuccessThreshold < 10) {
                    halfOpenSuccessThreshold += 2;
                    updateDelegateConfig();
                }
            }
            // Other statuses could have different adjustments
        } catch (Exception e) {
            logger.warn("Failed to adjust circuit breaker based on health", e);
        }
    }

    private void updateDelegateConfig() {
        // Implementation depends on circuit breaker library
    }

    // Delegate all CircuitBreaker methods
}
Enter fullscreen mode Exit fullscreen mode

Implementing Circuit Breaking with Modern Libraries

While understanding the principles is important, modern libraries provide battle-tested implementations. I've successfully used Resilience4j in many Java projects:

// Configure the circuit breaker
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .slowCallRateThreshold(50)
    .slowCallDurationThreshold(Duration.ofSeconds(2))
    .permittedNumberOfCallsInHalfOpenState(10)
    .minimumNumberOfCalls(10)
    .slidingWindowType(SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(100)
    .recordExceptions(IOException.class, TimeoutException.class)
    .ignoreExceptions(BusinessException.class)
    .build();

// Create a CircuitBreakerRegistry
CircuitBreakerRegistry circuitBreakerRegistry = 
    CircuitBreakerRegistry.of(circuitBreakerConfig);

// Create a CircuitBreaker
CircuitBreaker circuitBreaker = 
    circuitBreakerRegistry.circuitBreaker("paymentService");

// Decorate your service call with the CircuitBreaker
Supplier<PaymentResponse> decoratedSupplier = CircuitBreaker
    .decorateSupplier(circuitBreaker, () -> paymentService.processPayment(payment));

// Execute the decorated function
try {
    PaymentResponse response = decoratedSupplier.get();
    // Handle success
} catch (CircuitBreakerOpenException e) {
    // Handle circuit open
    return fallbackPaymentResponse();
} catch (Exception e) {
    // Handle other exceptions
    return errorPaymentResponse(e);
}
Enter fullscreen mode Exit fullscreen mode

Spring Cloud Circuit Breaker provides a uniform API across different circuit breaker implementations:

@Service
public class RecommendationService {
    private final RestTemplate restTemplate;
    private final CircuitBreakerFactory circuitBreakerFactory;

    public RecommendationService(RestTemplate restTemplate,
                               CircuitBreakerFactory circuitBreakerFactory) {
        this.restTemplate = restTemplate;
        this.circuitBreakerFactory = circuitBreakerFactory;
    }

    public List<Product> getRecommendedProducts(String userId) {
        CircuitBreaker circuitBreaker = circuitBreakerFactory.create("recommendations");

        return circuitBreaker.run(
            () -> restTemplate.getForObject("/recommendations/{id}", 
                    RecommendationResponse.class, userId).getProducts(),
            throwable -> getDefaultRecommendations()
        );
    }

    private List<Product> getDefaultRecommendations() {
        // Return cached or static recommendations
        return Collections.emptyList();
    }
}
Enter fullscreen mode Exit fullscreen mode

I've applied these patterns across many distributed Java systems, and they've proven invaluable during service degradation scenarios. By combining robust circuit breaking with proper fallbacks, even partial system failures can result in acceptable user experiences rather than complete outages.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)