Design circuit break pattern
Created on: Jan 28, 2025
In this blog, we will write a low level design to demonstrate circuit breaker patter using core java. We will be calling a mock service to demonstrate service call.
package com.example; /** * Circuit breaker implementation to prevent cascading failures in distributed * systems. * The circuit breaker has three states: CLOSED, OPEN, and HALF_OPEN. * CLOSED: The circuit is closed and requests are allowed to pass through. * OPEN: The circuit is open and requests are blocked. * HALF_OPEN: The circuit is half-open and allows a limited number of requests * to test if the issue is resolved. */ public class CircuitBreaker { public enum State { CLOSED, OPEN, HALF_OPEN } private final int failureThreshold; // Max failures before "open" private final int successThreshold; // Successes needed to "close" private volatile State state; // Current state private int failureCount; // Failure counter private int successCount; // Success counter private volatile long lastFailureTime; // Last failure timestamp private final long retryTimeout; // Retry timeout duration public CircuitBreaker(int failureThreshold, int successThreshold, long retryTimeout) { this.failureThreshold = failureThreshold; this.successThreshold = successThreshold; this.retryTimeout = retryTimeout; this.state = State.CLOSED; } // Synchronized to handle multi-threaded access public boolean allowRequest() { if (state == State.OPEN) { synchronized (this) { // Check if the retry timeout has elapsed if (System.currentTimeMillis() - lastFailureTime > retryTimeout) { state = State.HALF_OPEN; // Transition to HALF_OPEN state failureCount = 0; // Reset failure count successCount = 0; // Reset success count return true; // Allow the request } return false; // Block the request } } return true; // Allow requests in CLOSED and HALF_OPEN states } public void recordSuccess() { synchronized (this) { successCount++; if (successCount >= successThreshold) { state = State.CLOSED; // Transition to CLOSED state failureCount = 0; // Reset failure count } } } public void recordFailure() { synchronized (this) { failureCount++; if (failureCount >= failureThreshold) { state = State.OPEN; // Transition to OPEN state lastFailureTime = System.currentTimeMillis(); } } } public State getState() { State currentState; synchronized (this) { currentState = state; } return currentState; } }
We will test the application using below code.
package com.example; public class CircuitBreakerDemo { public static void main(String[] args) { CircuitBreaker circuitBreaker = new CircuitBreaker(3, 2, 5000); // 3 failures, 2 successes, 5 seconds timeout for (int i = 1; i <= 10; i++) { if (circuitBreaker.allowRequest()) { try { // Simulate a call to an external service if (simulateServiceCall(i)) { circuitBreaker.recordSuccess(); System.out.println("Call " + i + " succeeded."); } else { throw new RuntimeException("Simulated failure"); } } catch (Exception e) { circuitBreaker.recordFailure(); System.out.println("Call " + i + " failed."); } } else { System.out.println("Call " + i + " blocked by circuit breaker."); } } } private static boolean simulateServiceCall(int callNumber) { // Simulate a failure for specific calls with a more realistic approach // For example, fail every 4th call and every call that is a multiple of 5 return callNumber % 4 != 0 && callNumber % 5 != 0; } }
Unit test cases for same
package com.example; import static org.junit.jupiter.api.Assertions.*; import com.example.CircuitBreaker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.concurrent.TimeUnit; class CircuitBreakerTest { private CircuitBreaker circuitBreaker; @BeforeEach void setUp() { circuitBreaker = new CircuitBreaker(3, 2, 2000); // 3 failures to open, 2 successes to close, 2-second retry // timeout } @Test void testInitialStateIsClosed() { assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); } @Test void testRequestAllowedWhenClosed() { assertTrue(circuitBreaker.allowRequest()); } @Test void testTransitionToOpenStateOnFailures() { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); assertFalse(circuitBreaker.allowRequest()); } @Test void testRetryTimeoutTransitionToHalfOpen() throws InterruptedException { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); // Wait for retry timeout TimeUnit.MILLISECONDS.sleep(2100); assertTrue(circuitBreaker.allowRequest()); assertEquals(CircuitBreaker.State.HALF_OPEN, circuitBreaker.getState()); } @Test void testRequestAllowedWhenHalfOpen() throws InterruptedException { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); // Wait for retry timeout TimeUnit.MILLISECONDS.sleep(2100); assertTrue(circuitBreaker.allowRequest()); assertEquals(CircuitBreaker.State.HALF_OPEN, circuitBreaker.getState()); } @Test void testTransitionToClosedStateFromHalfOpen() throws InterruptedException { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); // Wait for retry timeout TimeUnit.MILLISECONDS.sleep(2100); // Simulate successful requests circuitBreaker.recordSuccess(); circuitBreaker.recordSuccess(); assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); } @Test void testTransitionBackToOpenStateFromHalfOpen() throws InterruptedException { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); // Wait for retry timeout TimeUnit.MILLISECONDS.sleep(2100); // Allow one request in HALF_OPEN state circuitBreaker.allowRequest(); // Simulate failure circuitBreaker.recordFailure(); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); } @Test void testFailureCountResetsAfterStateChange() { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); circuitBreaker.recordSuccess(); // Should reset counts when transitioning to HALF_OPEN circuitBreaker.recordSuccess(); assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); } @Test void testSuccessCountResetsAfterStateChange() throws InterruptedException { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); // Wait for retry timeout TimeUnit.MILLISECONDS.sleep(2100); circuitBreaker.allowRequest(); // Transition to HALF_OPEN circuitBreaker.recordFailure(); // Transition back to OPEN assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); circuitBreaker.recordSuccess(); // Reset counters circuitBreaker.recordSuccess(); assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); } @Test void testStateRemainsOpenWithinRetryTimeout() throws InterruptedException { circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); circuitBreaker.recordFailure(); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); // Wait less than the retry timeout TimeUnit.MILLISECONDS.sleep(1000); assertFalse(circuitBreaker.allowRequest()); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); } }
Checkout whole code in my github
