00:00

Circuit Breaker Pattern in Microservices

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, walks through a real e-commerce example, and then shows how to build one in Spring Boot with Resilience4j, including configuration, fallbacks, monitoring and testing.

1. The Problem: Cascading Failures

Splitting an application into microservices means a failure in one service no longer crashes the whole codebase. But it adds a new risk: services depend on each other over the network, and a slow dependency can quietly drag its callers down with it.

Here is how it happens:

  1. Service C becomes slow, for example because its database is overloaded.
  2. Service B calls C and waits. Each waiting request holds a thread.
  3. New requests keep arriving, and soon all of B’s threads are stuck waiting for C.
  4. B can no longer answer anyone, so Service A, which calls B, starts waiting too.

One slow service has now made three services unavailable. This chain reaction is called a cascading failure, and it is exactly what the circuit breaker is designed to stop.

2. What Is the Circuit Breaker Pattern?

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.

A software circuit breaker sits between your service and a remote dependency. It watches every call. When too many calls fail, it trips and stops sending requests to that dependency for a while, returning a quick error or a fallback response instead.

Electrical circuit breakerSoftware circuit breaker
Monitors the electrical currentMonitors calls to a remote service
Trips when the current is too highOpens when too many calls fail or are too slow
Cuts power to protect the wiringBlocks calls to protect both services
Is reset once the fault is fixedTests the service and closes again once it recovers

The goal is simple: fail fast instead of failing slowly. A quick, clear failure is far better than threads piling up while they wait for a service that will not answer.

3. The Three States of a Circuit Breaker

A circuit breaker is a small state machine with three main states:

StateWhat happens to callsMoves to
CLOSED (normal)Calls go through to the remote service. The breaker records successes and failures.OPEN when the failure rate passes the threshold
OPEN (tripped)Calls are rejected immediately, without contacting the service. A fallback runs instead.HALF-OPEN after a waiting period
HALF-OPEN (testing)A small number of trial calls are allowed through to check whether the service has recovered.CLOSED if the trial calls succeed, or back to OPEN if they fail
State transitions
            failure rate too high
   CLOSED ─────────────────────────▶ OPEN
     ▲                                │
     │                                │ wait duration ends
     │   trial calls succeed          ▼
     └──────────────────────────── HALF-OPEN
                                      │
                OPEN ◀────────────────┘
                     trial calls fail

Note that “closed” means calls flow, just like a closed electrical circuit lets current flow. “Open” means the circuit is broken and calls are blocked. Resilience4j also has special states such as FORCED_OPEN and DISABLED, which operators can set by hand.

4. A Practical Example: E-commerce Application

Consider an online shop with three services:

ServiceResponsibility
Order ServiceCreates customer orders
Payment ServiceCharges the customer; an important but sometimes unreliable remote dependency
Inventory ServiceTracks product stock levels

4.1 Without a circuit breaker

The Payment Service develops a database problem and starts taking 30 seconds to respond:

  • The first order request waits 30 seconds for the payment call to time out.
  • Meanwhile, new orders keep arriving, and each one also waits on the Payment Service.
  • Within moments, every thread in the Order Service is blocked.
  • The Order Service can no longer respond to anything, not even health checks or simple order look-ups, although its own code is perfectly healthy.

4.2 With a circuit breaker

Now a circuit breaker wraps the call to the Payment Service:

  1. CLOSED: payments work normally and the breaker records each result.
  2. CLOSED → OPEN: the Payment Service slows down. Of the last 10 calls, 6 fail or exceed the slow-call limit. That 60% failure rate passes the 50% threshold, so the breaker opens.
  3. OPEN: new payment calls are rejected instantly, with no waiting. The Order Service runs a fallback: it saves the order as “Payment Pending” and tells the customer, “Your order is saved. We will confirm payment shortly.” Everything else in the Order Service keeps working.
  4. OPEN → HALF-OPEN: after 30 seconds, the breaker lets 3 trial payment calls through.
  5. HALF-OPEN → CLOSED: the trial calls succeed, so the breaker closes and normal payments resume. If they had failed, it would open again for another 30 seconds.

The shop stays online and orders are not lost. The Payment Service also gets breathing room to recover, instead of being flooded with requests while it is struggling.

5. Implementing a Circuit Breaker with Resilience4j

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.

5.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.

5.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.

5.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.

6. 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

7. Fallback Strategies

A fallback decides what the user gets when the real call cannot be made. The right choice depends on the feature:

StrategyExample
Return cached dataShow the last known product price or stock level
Return a safe defaultShow general recommendations instead of personalised ones
Save and process laterMark the order “Payment Pending” and retry in the background
Degrade the featureHide the reviews section while the review service is down
Fail with a clear message“Payments are temporarily unavailable. Please try again in a few minutes.”

Here, a product service falls back to cached data:

InventoryClient.java
@Component
public class InventoryClient {

    private final RestClient restClient;
    private final StockCache stockCache;

    public InventoryClient(RestClient.Builder builder, StockCache stockCache) {
        this.restClient = builder.baseUrl("http://inventory-service").build();
        this.stockCache = stockCache;
    }

    @CircuitBreaker(name = "inventoryService", fallbackMethod = "cachedStock")
    public StockLevel getStock(String sku) {
        StockLevel stock = restClient.get()
                .uri("/api/stock/{sku}", sku)
                .retrieve()
                .body(StockLevel.class);
        stockCache.put(sku, stock); // keep a copy for bad times
        return stock;
    }

    private StockLevel cachedStock(String sku, CallNotPermittedException ex) {
        // circuit is open: use the last known value
        return stockCache.get(sku).orElse(StockLevel.unknown(sku));
    }

    private StockLevel cachedStock(String sku, Exception ex) {
        // the call itself failed: same answer
        return stockCache.get(sku).orElse(StockLevel.unknown(sku));
    }
}

You can define several fallback methods with different exception types. Resilience4j picks the one whose exception parameter matches most closely, so you can react differently to “circuit open” and “call failed”.

8. Combining with Retry, Timeout and Bulkhead

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

PatternProtects againstLearn more
Circuit BreakerRepeatedly calling a service that is downThis page
RetryShort, temporary glitchesRetry Pattern
Timeout (TimeLimiter)Waiting too long for one callTimeout Pattern
BulkheadOne dependency using up all threadsBulkhead Pattern
Rate LimiterSending more requests than allowedRate Limiting Pattern

Resilience4j lets you stack them on one method:

PaymentClient.java
@Retry(name = "paymentService")
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Bulkhead(name = "paymentService")
public PaymentResult charge(PaymentRequest request) {
    return restClient.post()
            .uri("/api/payments")
            .body(request)
            .retrieve()
            .body(PaymentResult.class);
}
application.yml
resilience4j:
  retry:
    instances:
      paymentService:
        maxAttempts: 3
        waitDuration: 500ms
        ignoreExceptions:
          - io.github.resilience4j.circuitbreaker.CallNotPermittedException
  bulkhead:
    instances:
      paymentService:
        maxConcurrentCalls: 20

By default, Resilience4j applies them in this order, from outside to inside:

Order of execution
Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( your method ) ) ) ) )

Because Retry is outermost, each retry attempt passes through the circuit breaker and is counted. Ignoring CallNotPermittedException in Retry is important: when the circuit is open, retrying is pointless and only adds delay.

Always set network timeouts as well, either on the HTTP client or with a TimeLimiter. Without them, a “slow” call might hang far longer than slowCallDurationThreshold.

9. Monitoring Circuit Breakers

Expose the circuit breaker details through Actuator:

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.

10. 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.

11. 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.

12. 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

13. 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.

14. 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.