00:00

Circuit Breaker Pattern in Microservices

Last updated:

In a microservices system, services constantly call each other over the network. Sooner or later, one of them will become slow or stop responding. The Circuit Breaker pattern stops that one problem from spreading and taking down every service that depends on it.

This tutorial explains how a circuit breaker works and shows how to build one in Spring Boot with Resilience4j, including configuration, fallbacks, monitoring and testing.

1. What Is the Circuit Breaker Pattern?

The Circuit Breaker Pattern is one of the critical resilience and fault-tolerance patterns used in microservices, where different services interact with each other over a network, and the circuit breaker protects them from cascading failures to enhance the resilience and fault tolerance of a distributed system.

Cascading Failure: In a microservices, services often depend on one another. If one service becomes unavailable or responds slowly, dependent services may experience increased latency, resource exhaustion, or failures. This chain reaction is known as a cascading failure. The Circuit Breaker Pattern helps prevent this by temporarily blocking calls to the failing service and allowing it time to recover.

The pattern is named after the circuit breaker in your home’s electrical panel. When too much current flows, the breaker “trips” and cuts the power, protecting the wiring from damage. Once the problem is fixed, you switch it back on.

2. The Three States of a Circuit Breaker

The Circuit Breaker pattern in microservices maintains three states: Closed, Open, and Half-Open to prevent repeated calls to failing services and protect the system from cascading failures.

  • CLOSED: Closed is the initial, normal state. All requests pass through to the other microservices smoothly, and the circuit breaker records each call’s outcome in a time-based or count-based sliding window. When the failure rate reaches the configured threshold, it switches to Open and rejects further calls immediately, so the application can return a fallback response.

    Circuit breaker in the CLOSED state The payment service is healthy and the circuit breaker is closed. The order service's request passes through the breaker to the payment service, and the response comes back the same way. The breaker records the result of every call. Payment Service healthy → breaker CLOSED request call forwarded response response CLOSED Order Service caller Circuit Breaker lever up · counting calls Payment Service responding normally
    Figure 1: CLOSED: the Payment Service is healthy, so the circuit breaker forwards every call and records whether it succeeded or failed.
  • OPEN: In this state, the circuit breaker stops sending requests to the failing service. Calls are rejected immediately (fail fast), and the application can return a fallback response. After a configured wait duration, the circuit breaker moves to Half-Open state.

    Circuit breaker in the OPEN state The payment service is failing and the circuit breaker is open. Calls to the payment service are blocked, and the breaker returns a cached or default fallback response to the order service immediately while the payment service recovers. Payment Service failing → breaker OPEN request call blocked returned immediately Fallback cached or default response OPEN Order Service caller Circuit Breaker lever down · fail fast Payment Service timeouts / errors after a wait → HALF-OPEN
    Figure 2: OPEN: the Payment Service keeps failing, so the breaker blocks calls and returns a fallback at once instead of waiting for timeouts, giving the service time to recover.
  • HALF-OPEN: It is the recovery-check state. The circuit breaker allows a limited number of trial calls through to the downstream service and rejects the rest. If the failure rate of these trial calls stays below the threshold, it switches back to Closed. Otherwise, it returns to Open and waits again.

    Circuit breaker in the HALF-OPEN state After the wait duration, the circuit breaker is half-open and lets only a few trial calls through to the recovering payment service. If the trial calls succeed the breaker closes; if they fail it opens again. Calls beyond the trial limit still receive the fallback. Wait duration over → breaker HALF-OPEN request a few trial calls only HALF-OPEN Order Service caller Circuit Breaker lever half-way · testing Payment Service recovering? succeed → CLOSED fail → OPEN Extra calls still get the fallback.
    Figure 3: HALF-OPEN: after the wait duration, a few trial calls test the Payment Service. If they succeed the breaker closes again; if they fail it goes back to open.

3. Implementing a Circuit Breaker with Resilience4j in Spring Boot Application

Resilience4j is a lightweight, open-source fault-tolerance library for Java and the most common choice for Spring Boot. It replaced Netflix Hystrix, which is no longer developed.

3.1 Add the dependencies

pom.xml
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

The AOP starter lets the annotations work, and Actuator adds health and metrics endpoints. Set resilience4j.version to the latest release in your pom.xml properties.

3.2 Configure the circuit breaker

application.yml
resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 5
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s
        slowCallRateThreshold: 50
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 3
        automaticTransitionFromOpenToHalfOpenEnabled: true
        recordExceptions:
          - org.springframework.web.client.ResourceAccessException
          - org.springframework.web.client.HttpServerErrorException
        ignoreExceptions:
          - com.javabytechie.shop.payment.PaymentDeclinedException

In plain words: look at the last 10 calls; once at least 5 have been made, open the circuit if half of them failed or took longer than 2 seconds; stay open for 30 seconds; then allow 3 trial calls. Each setting is explained in the next section.

3.3 Protect the remote call

PaymentClient.java
package com.javabytechie.shop.payment;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class PaymentClient {

    private static final Logger log = LoggerFactory.getLogger(PaymentClient.class);

    private final RestClient restClient;

    public PaymentClient(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("http://payment-service").build();
    }

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    public PaymentResult charge(PaymentRequest request) {
        return restClient.post()
                .uri("/api/payments")
                .body(request)
                .retrieve()
                .body(PaymentResult.class);
    }

    // Same return type and parameters, plus the exception as the last parameter
    private PaymentResult paymentFallback(PaymentRequest request, Throwable ex) {
        log.warn("Payment unavailable for order {}: {}", request.orderId(), ex.getMessage());
        return PaymentResult.pending(request.orderId());
    }
}
OrderService.java
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentClient paymentClient;

    public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
        this.orderRepository = orderRepository;
        this.paymentClient = paymentClient;
    }

    @Transactional
    public Order placeOrder(OrderRequest request) {
        Order order = orderRepository.save(Order.newOrder(request));

        PaymentResult payment = paymentClient.charge(PaymentRequest.from(order));

        if (payment.isPending()) {
            order.markPaymentPending(); // a background job retries these later
        } else {
            order.markPaid(payment.transactionId());
        }
        return order;
    }
}

The name in @CircuitBreaker must match the instance name in application.yml. While the circuit is open, the fallback receives a CallNotPermittedException, which tells you the call was blocked by the breaker rather than failed by the service.

4. Configuration Explained

SettingWhat it meansDefault
slidingWindowTypeJudge the last N calls (COUNT_BASED) or the last N seconds (TIME_BASED)COUNT_BASED
slidingWindowSizeHow many calls (or seconds) to look at100
minimumNumberOfCallsCalls needed before the failure rate is calculated at all100
failureRateThresholdFailure percentage that opens the circuit50
slowCallDurationThresholdCalls slower than this count as slow60s
slowCallRateThresholdSlow-call percentage that opens the circuit100
waitDurationInOpenStateHow long to stay open before testing again60s
permittedNumberOfCallsInHalfOpenStateTrial calls allowed in half-open state10
automaticTransitionFromOpenToHalfOpenEnabledMove to half-open on a timer, without waiting for a new callfalse
recordExceptionsExceptions that count as failuresAll exceptions
ignoreExceptionsExceptions that count as neither success nor failureNone

The defaults are cautious, so most applications tune them. Two settings deserve special attention:

  • minimumNumberOfCalls stops the circuit from opening because of one or two unlucky calls, for example 1 failure out of 1 call, which is a 100% failure rate.
  • ignoreExceptions should list business errors, such as a declined card. A declined card means the Payment Service is working correctly, so it must not push the circuit towards opening.

You can share settings between several instances using configs.default, then override only what differs per instance:

application.yml
resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 30s
    instances:
      paymentService:
        baseConfig: default
        waitDurationInOpenState: 60s
      inventoryService:
        baseConfig: default

5. Combining Circuit Breaker with Retry, Timeout and Bulkhead Patterns

A circuit breaker works best alongside other resilience patterns. Each one solves a different problem:

PatternDefinitionProtects against
Circuit BreakerWatches calls to a dependency and, when too many fail, stops calling it for a while and returns a fallback instead.Repeatedly calling a service that is down
RetryAutomatically repeats a failed call a limited number of times, with a short wait between attempts.Short, temporary glitches
Timeout (TimeLimiter)Sets a maximum time a call may take and gives up on it once that limit is reached.Waiting too long for one call
BulkheadLimits how many calls to a dependency can run at the same time, keeping its resources separate from the rest of the service.One dependency using up all threads
Rate LimiterCaps how many calls can be made in a given period, such as 100 requests per second.Sending more requests than allowed

6. Monitoring Circuit Breakers with Spring Boot Actuator

Enable the Actuator endpoints to view each circuit breaker’s state, failure rate and recent events.

application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,circuitbreakers,circuitbreakerevents,metrics
  endpoint:
    health:
      show-details: always
  health:
    circuitbreakers:
      enabled: true
EndpointShows
GET /actuator/healthEach circuit breaker with its state and failure rate
GET /actuator/circuitbreakersAll circuit breakers and their current state
GET /actuator/circuitbreakereventsRecent events: successes, errors, state changes
GET /actuator/metrics/resilience4j.circuitbreaker.stateCurrent state as a number, for dashboards

A response from the health endpoint looks like this:

Response
{
  "status": "UP",
  "components": {
    "circuitBreakers": {
      "status": "UP",
      "details": {
        "paymentService": {
          "status": "UP",
          "details": {
            "state": "CLOSED",
            "failureRate": "10.0%",
            "slowCallRate": "0.0%",
            "bufferedCalls": 10,
            "failedCalls": 1,
            "notPermittedCalls": 0
          }
        }
      }
    }
  }
}

Set up an alert for every transition to OPEN. An open circuit means a dependency is in trouble, and someone should know about it right away.

7. Testing a Circuit Breaker

Test that the fallback really works, by forcing the circuit open instead of waiting for real failures:

PaymentClientCircuitBreakerTest.java
@SpringBootTest
class PaymentClientCircuitBreakerTest {

    @Autowired
    private PaymentClient paymentClient;

    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;

    @AfterEach
    void resetCircuit() {
        circuitBreakerRegistry.circuitBreaker("paymentService").reset();
    }

    @Test
    void returnsPendingResultWhenCircuitIsOpen() {
        circuitBreakerRegistry.circuitBreaker("paymentService").transitionToOpenState();

        PaymentResult result = paymentClient.charge(new PaymentRequest(1001L, new BigDecimal("49.99")));

        assertThat(result.isPending()).isTrue();
    }
}

To test the transitions themselves, point the client at a mock server, such as WireMock, that returns errors. Make a few calls and check that the state changes from CLOSED to OPEN. Small values for slidingWindowSize and waitDurationInOpenState in a test profile keep such tests fast.

8. Best Practices

  • Use one circuit breaker per dependency, such as paymentService and inventoryService, so one failing service does not block calls to healthy ones.
  • Count only technical failures. Put business errors such as “card declined” or “not found” in ignoreExceptions.
  • Treat slow calls as failures with slowCallDurationThreshold, because a slow service is often worse than a down one.
  • Always provide a meaningful fallback, even if it is just a clear error message.
  • Combine with timeouts, so calls cannot hang without limit.
  • Tune settings using real traffic data. A window of 10 calls suits a busy service, but may react too slowly for a rarely called one.
  • Monitor and alert on state changes.
  • Keep fallbacks simple and fast. A fallback that calls another unreliable service just moves the problem.

9. Common Mistakes to Avoid

MistakeWhat goes wrongFix
Fallback signature does not matchStartup or runtime error: fallback method not foundSame return type and parameters, plus an exception parameter last
Calling the protected method from the same classThe annotation is skipped, so there is no protectionCall it through another bean
Counting business errors as failuresThe circuit opens although the service is healthyAdd them to ignoreExceptions
Leaving minimumNumberOfCalls at 100 on a quiet serviceThe circuit never opensLower it to fit real traffic
Retrying while the circuit is openWasted time and extra delayIgnore CallNotPermittedException in Retry
One shared breaker for all servicesOne failure blocks every callOne instance per dependency
No timeouts on the HTTP clientCalls hang far beyond the slow-call limitSet connect and read timeouts

10. Key Benefits

  • Prevents cascading failures: a problem in one service stays in that service.
  • Graceful degradation: users get a useful, if limited, response instead of an error page or a spinning wheel.
  • Saves resources: failing fast frees threads, connections and memory that would otherwise wait on doomed calls.
  • Helps the failing service recover: fewer incoming requests give it room to get back on its feet.
  • Automatic recovery: the half-open state detects when the service is healthy again, with no manual action.
  • Better visibility: circuit states and events show exactly which dependency is causing trouble.

11. Conclusion

Failures are normal in distributed systems; what matters is how your system reacts to them. The Circuit Breaker pattern watches calls to each dependency, opens when too many fail, answers instantly with a fallback while the dependency recovers, and closes again once trial calls succeed. In Spring Boot, Resilience4j makes this a matter of a dependency, a few lines of configuration and one annotation. Combine it with timeouts, retries and bulkheads, monitor it closely, and your services will bend under pressure instead of breaking.