Why Circuit breaker?
Circuit breaker Prevents cascading failures across microservices and Improves user experience by failing fast instead of hanging. Supports graceful degradation with fallbacks (e.g., cached responses, safe defaults).


   ┌───────────────┐
   │               │
   │    CLOSED     │  ←─── Successes reset failure count
   │ (Normal flow) │
   └───────┬───────┘
           │
           │ Too many failures
           ▼
   ┌───────────────┐
   │               │
   │     OPEN      │  ←─── All calls blocked, fallback used
   │ (Protection)  │
   └───────┬───────┘
           │
           │ Wait duration expires
           ▼
   ┌───────────────┐
   │               │
   │   HALF-OPEN   │  ←─── Limited test calls allowed
   │ (Trial mode)  │
   └───────┬───────┘
           │
   ┌───────┴───────────────┐
   │                       │
   ▼                       ▼
Success → CLOSED   Failures → back to OPEN


How Circuit breaker Works?
It cycles through three states — Closed, Open, and Half‑Open — to balance resilience with recovery.

Closed State – Requests flow normally to the downstream service, tracks metrics like error rate, timeouts, and latency.
Open State – Triggered when failures exceed the configured threshold, breaker “trips” and immediately rejects further calls.This prevents wasted retries and protects system resources.
Half‑Open State – After a cooldown period, the breaker allows limited trial requests. If these succeed → breaker resets to Closed. If they fail → breaker returns to Open for another cooldown.

What is the Default condition of Circuit breaker to move to open state?
For Resilence4j Failure Rate Threshold: 50% with a sliding window size of 100 Request. I.E. If 50 calls of last 100 request fails then circuit breaker moves to open state. Wait Duration in Open State: 60 seconds. After 60 Seconds Circuit breaker comes to half open state with 10 trial calls to test recovery before deciding to close or reopen state.

Sample config of Circuit Breaker which has sliding window of size 8 and failure call limit of 4. The CB would be OPEN state for 2 Seconds and permits 3 calls in HALFOPEN before turning to CLOSED state

resilience4j.circuitbreaker.instances.EmployeeService.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.EmployeeService.slidingWindowSize=8
resilience4j.circuitbreaker.instances.EmployeeService.failureRateThreshold=4
resilience4j.circuitbreaker.instances.EmployeeService.waitDurationInOpenState=2s
resilience4j.circuitbreaker.instances.EmployeeService.permittedNumberOfCallsInHalfOpenState=3

From postman 20 request sent with delay interval of 0.3 seconds

Count Request URL Seconds Http Status CB State Comments
1 Req1 http://localhost:8085/empmgmt/test/employees/ise 0.3 500 Server Error CLOSED
2 Req2 http://localhost:8085/empmgmt/test/employees/ise 0.6 500 Server Error CLOSED
3 Req3 http://localhost:8085/empmgmt/test/employees/ise 0.9 500 Server Error CLOSED
4 Req4 http://localhost:8085/empmgmt/test/employees/ise 1.2 500 Server Error CLOSED
5 Req5 http://localhost:8085/empmgmt/test/employees/ise 1.5 500 Server Error CLOSED
6 Req6 http://localhost:8085/empmgmt/test/employees/ise 1.8 500 Server Error CLOSED
7 Req7 http://localhost:8085/empmgmt/test/employees/ise 2.1 500 Server Error CLOSED
8 Req8 http://localhost:8085/empmgmt/test/employees/ise 2.4 Fallback Method OPEN CB Opens as Sliding Window Size is 8 Request
9 Req9 http://localhost:8085/empmgmt/test/employees 2.7 Fallback Method OPEN
10 Req10 http://localhost:8085/empmgmt/test/employees 3 Fallback Method OPEN
11 Req11 http://localhost:8085/empmgmt/test/employees 3.3 Fallback Method OPEN
12 Req12 http://localhost:8085/empmgmt/test/employees 3.6 Fallback Method OPEN
13 Req13 http://localhost:8085/empmgmt/test/employees 3.9 201 OK HALF-OPEN CB comes to half open after waiting 2 Seconds
14 Req14 http://localhost:8085/empmgmt/test/employees 4.2 201 OK HALF-OPEN
15 Req15 http://localhost:8085/empmgmt/test/employees 4.5 201 OK HALF-OPEN CB test 3 Request in HALF OPEN State
16 Req16 http://localhost:8085/empmgmt/test/employees 4.8 201 OK CLOSED CB comes to CLOSED State if 3 Request are Successful
17 Req17 http://localhost:8085/empmgmt/test/employees 5.1 201 OK CLOSED
18 Req18 http://localhost:8085/empmgmt/test/employees 5.4 201 OK CLOSED
19 Req19 http://localhost:8085/empmgmt/test/employees 5.7 201 OK CLOSED
20 Req20 http://localhost:8085/empmgmt/test/employees 6 201 OK CLOSED

How it works

  1. @Autowired private CircuitBreakerRegistry circuitBreakerRegistry;
    Spring injects the registry that holds all configured circuit breakers. You can fetch a breaker by name from here.
  2. @PostConstruct
    This method runs automatically after the bean is initialized, ensuring your event listeners are registered before the service starts handling requests.
  3. circuitBreakerRegistry.circuitBreaker(EMPLOYEE_SERVICE);
    Retrieves the circuit breaker instance named EMPLOYEE_SERVICE. That name must match what you configured in application.yml.

EmployeeService.java

@RestController
public class EmployeeController {
    @Autowired
    EmployeeService employeeService;

    @GetMapping("/employees")
    public String getEmployees() {
        ResponseEntity<String> response = employeeService.getEmployeeDetails();
        return response.getBody();
    }
}

EmployeeService.java

@Service
@Slf4j
public class EmployeeService {
    private final RestTemplate restTemplate;

    private static final String EMPLOYEE_SERVICE = "EmployeeService";

    AtomicInteger count = new AtomicInteger(1);

    @Autowired
    public EmployeeService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;

    @PostConstruct
    public void registerCircuitBreakerEvents() {
        // Retrieve the CircuitBreaker instance from the registry
        // The name EMPLOYEE_SERVICE must match the one defined in application.yml
        io.github.resilience4j.circuitbreaker.CircuitBreaker cb =
                circuitBreakerRegistry.circuitBreaker(EMPLOYEE_SERVICE);

        // Attach a listener to the CircuitBreaker’s event publisher
        // This listener will be triggered whenever the breaker changes state
        cb.getEventPublisher()
                .onStateTransition(event -> {
                    // Cast the generic event to a specific CircuitBreakerOnStateTransitionEvent
                    CircuitBreakerOnStateTransitionEvent transitionEvent =
                            (CircuitBreakerOnStateTransitionEvent) event;

                    // Check if the breaker has moved to the OPEN state
                    if (transitionEvent.getStateTransition().getToState() ==
                            io.github.resilience4j.circuitbreaker.CircuitBreaker.State.OPEN) {
                        // Log a warning when the breaker trips OPEN (calls are blocked)
                        log.warn("⚠️ CircuitBreaker [{}] moved to OPEN state at {}",
                                cb.getName(), transitionEvent.getCreationTime());
                    } else {
                        // Log info for other transitions (CLOSED → HALF_OPEN, HALF_OPEN → CLOSED, etc.)
                        log.info("CircuitBreaker [{}] transitioned: {}",
                                cb.getName(), transitionEvent.getStateTransition());
                    }
                });
    }


    // Use the annotation here
    @io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker(name = EMPLOYEE_SERVICE, fallbackMethod = "getEmployeeFallback")
    public ResponseEntity<String> getEmployeeDetails() {
        String url = "http://localhost:8085/empmgmt/test/employees?delay=500";

        if(count.get() <=8){
            url = "http://localhost:8085/empmgmt/test/employees/ise?delay=500";
        }else{
            url = "http://localhost:8085/empmgmt/test/employees?delay=2000";
        }

        log.info("Count value {}", count.get());

        long startTime = System.nanoTime();

        ResponseEntity<String> response =
                restTemplate.getForEntity(url, String.class);

        count.incrementAndGet();

        long endTime = System.nanoTime();
        long durationMillis = (endTime - startTime) / 1_000_000;

        log.info("Downstream call took: {} ms", durationMillis);

        return response;
    }

    public ResponseEntity<String> getEmployeeFallback(Throwable t) {
        log.error("Fallback triggered due to exception: {} Count is {} ", t.getMessage(), count.get());
        count.incrementAndGet();
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                .body("Service unavailable");
    }
}

Logs in Circuit Breaker

Count value 1
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 1 
Count value 2
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 2 
Count value 3
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 3 
Count value 4
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 4 
Count value 5
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 5 
Count value 6
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 6 
Count value 7
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 7 
Count value 8
⚠️ CircuitBreaker [EmployeeService] moved to OPEN state at 2026-08-16T14:16:53.120914500+05:30[Asia/Calcutta]
Fallback triggered due to exception: 500 Server Error on GET request for "http://localhost:8085/empmgmt/test/employees/ise": "{ "message": "500 Internal Server Error"}" Count is 8 
Fallback triggered due to exception: CircuitBreaker 'EmployeeService' is OPEN and does not permit further calls Count is 9 
Fallback triggered due to exception: CircuitBreaker 'EmployeeService' is OPEN and does not permit further calls Count is 10 
Fallback triggered due to exception: CircuitBreaker 'EmployeeService' is OPEN and does not permit further calls Count is 11 
Fallback triggered due to exception: CircuitBreaker 'EmployeeService' is OPEN and does not permit further calls Count is 12 
CircuitBreaker [EmployeeService] transitioned: State transition from OPEN to HALF_OPEN
Count value 13
Downstream call took: 2009 ms
Count value 14
Downstream call took: 2006 ms
Count value 15
Downstream call took: 2008 ms
CircuitBreaker [EmployeeService] transitioned: State transition from HALF_OPEN to CLOSED
Count value 16
Downstream call took: 2012 ms
Count value 17
Downstream call took: 2008 ms
Count value 18
Downstream call took: 2016 ms
Count value 19
Downstream call took: 2013 ms
Count value 20
Downstream call took: 2013 ms