Circuit Breaker in Spring Boot Microservices Explained Like You’re a Dog 🐢

 


1. Imagine You’re a Dog Living in a House πŸ•

Hey, buddy! Imagine you’re a dog living in a big house.

Every day, your owner gives you biscuits.

One morning, you feel hungry and bark at your owner.

Your owner doesn’t hear you.

You bark again.

Nothing happens!

You bark ten more times, but your owner is still busy.

Now you’re exhausted, and your owner is getting annoyed by all the barking.

Wouldn’t it be better to stop barking for a while and try again later?

Of course!

You decide to wait for a little while. After some time, you bark once again to check whether your owner is available.

This time, your owner hears you and gives you a biscuit.

Congratulations! 🐢

You have just understood the basic idea behind the Circuit Breaker Pattern.

In software engineering, a Circuit Breaker helps prevent an application from repeatedly calling a downstream service that is likely to fail.

Instead of continuously making unsuccessful calls, the application temporarily stops sending requests to that service and allows it time to recover.

Let’s understand how this works in Spring Boot microservices.

2. What Is the Circuit Breaker Pattern?

Imagine we have two microservices:

  • Order Service: Receives customer requests.
  • Product Service: Provides product information.

When a customer requests an order, Order Service calls Product Service.

Client
   |
   v
Order Service
   |
   v
Product Service
   |
   v
Product Details

Everything works perfectly when Product Service is healthy.

But what happens when Product Service goes down?

Order Service continues calling it.

Every request fails, consumes resources, and may wait for a timeout before returning an error.

If thousands of requests arrive, Order Service can waste a significant amount of time and resources calling a service that is unavailable.

Repeated calls can also increase the load on Product Service and make recovery more difficult.

We need a smarter solution.

The Circuit Breaker Pattern prevents repeated calls to a downstream service when failures indicate that the service may be unhealthy.

Think of it like an electrical circuit breaker in your house.

When an electrical problem occurs, the breaker interrupts the circuit to protect the system.

Similarly, a software Circuit Breaker can temporarily stop calls to an unhealthy downstream service.

3. Why Do We Need Circuit Breaker in Microservices?

Let’s understand the problem with a simple example.

Imagine Product Service has stopped working.

Order Service receives five requests.

Without a Circuit Breaker, the application may make five unsuccessful calls.

Order Service
     |
     +---- Call 1 ----> Product Service ❌
     |
     +---- Call 2 ----> Product Service ❌
     |
     +---- Call 3 ----> Product Service ❌
     |
     +---- Call 4 ----> Product Service ❌
     |
     +---- Call 5 ----> Product Service ❌

What are the problems?

  1. Order Service wastes resources on unsuccessful calls.
  2. Requests may experience unnecessary latency.
  3. Threads can remain occupied while waiting for timeouts.
  4. Product Service receives additional traffic while it is struggling to recover.

Now imagine we introduce a Circuit Breaker.

After enough failures are detected, the Circuit Breaker opens.

Order Service
     |
     v
Circuit Breaker
     |
     X Circuit is OPEN
     |
     v
Calls rejected immediately

Instead of making another downstream call, the application can fail fast or return an appropriate fallback response.

After a configured waiting period, the Circuit Breaker can allow a few test calls to check whether Product Service has recovered.

This is the core idea behind Circuit Breaker.

4. The Three States of Circuit Breaker

Resilience4j Circuit Breaker has three main states:

  1. CLOSED
  2. OPEN
  3. HALF_OPEN

Imagine these states as three different rules your owner gives you about asking for biscuits.

4.1 CLOSED State

Your owner is available and giving you biscuits.

You are allowed to bark whenever you need one.

This is the normal operating state.

In a Circuit Breaker, CLOSED means calls are allowed to reach the downstream service.

The Circuit Breaker monitors the results of those calls.

If the configured failure threshold is exceeded, it transitions to OPEN.

Order Service
     |
     v
Circuit Breaker
  CLOSED
     |
     v
Product Service

Remember: CLOSED does not mean the service is closed or unavailable. It means the circuit allows calls.

4.2 OPEN State

Now imagine your owner is sleeping.

You keep barking, but nobody responds.

Finally, you decide to stop barking for a while.

This is similar to the OPEN state.

When the Circuit Breaker opens, calls to the protected downstream operation are blocked or rejected without making the normal downstream call.

Order Service
     |
     v
Circuit Breaker
    OPEN
     |
     X
     |
No normal call to Product Service

The application can use a fallback response if one is configured.

For example:

“Product Service is temporarily unavailable. Please try again later.”

The Circuit Breaker remains OPEN for the configured waiting period.

This protects the downstream service from repeated calls and prevents the caller from wasting resources on calls that are likely to fail.

4.3 HALF_OPEN State

After sleeping for some time, you wake up.

You want to check whether your owner is available again.

Instead of barking continuously, you bark a few times and observe what happens.

This is similar to the HALF_OPEN state.

After the waiting period, the Circuit Breaker allows a limited number of test calls to the downstream service.

If those calls succeed according to the configured criteria, the circuit can return to CLOSED.

If the failure criteria are still met, it can return to OPEN.

             CLOSED
                |
       Failure threshold exceeded
                |
                v
              OPEN
                |
        Wait duration expires
                |
                v
            HALF_OPEN
             /       \
       Successful   Failures
          tests        |
            |          |
            v          v
         CLOSED       OPEN

The HALF_OPEN state helps determine whether the downstream service has recovered without immediately allowing unrestricted traffic.

5. CLOSED vs. OPEN vs. HALF_OPEN

Let’s compare the three states.

Feature CLOSED OPEN HALF_OPEN
Calls allowed? Yes Normal downstream calls are blocked Limited test calls
Main purpose Normal operation and monitoring Protect against repeated failures Check whether the service recovered
What happens after success? Continues normally Waits for the configured transition Can return to CLOSED
What happens after failures? May transition to OPEN Remains OPEN Can transition back to OPEN

The easiest way to remember the states is:

  • CLOSED: Everything is normal.
  • OPEN: Stop calling the unhealthy service.
  • HALF_OPEN: Allow a few test calls to check recovery.

Now let’s implement this using Spring Boot and Resilience4j.

6. Implement Circuit Breaker in Spring Boot

Imagine we have an Order Service that calls Product Service using a Feign Client.

We want to configure a Circuit Breaker that opens when the observed failure rate reaches a threshold.

Step 1: Add the Resilience4j dependency

Add this dependency to your Maven pom.xml:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.1.0</version>
</dependency>

Use a version compatible with your Spring Boot application.

Step 2: Create the Product Feign Client

@FeignClient(name = "product-service")
public interface ProductClient {

    @GetMapping("/products/{id}")
    String getProductById(
        @PathVariable("id") String id
    );
}

This interface allows Order Service to communicate with Product Service through a Feign Client.

The service name product-service can be resolved through the service-discovery and load-balancing setup in your application.

Step 3: Create the Order Service

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final ProductClient productClient;

    public OrderService(ProductClient productClient) {
        this.productClient = productClient;
    }

    @CircuitBreaker(
        name = "productService",
        fallbackMethod = "productFallback"
    )
    public String invokeProductAPI(String id) {

        return productClient.getProductById(id);
    }

    public String productFallback(
            String id,
            Throwable ex) {

        return "Product Service is temporarily unavailable.";
    }
}

Let’s understand the important parts.

@CircuitBreaker

This annotation enables Circuit Breaker protection for the method when Resilience4j’s Spring integration is configured.

name = "productService"

This identifies the Circuit Breaker instance whose configuration we will define in application.properties.

fallbackMethod = "productFallback"

This specifies the fallback method to use when the protected call fails and the exception is handled by the fallback mechanism.

Important: The fallback signature should match the original method’s arguments, followed optionally by a compatible exception parameter. Since invokeProductAPI accepts a String id, the fallback accepts the same ID followed by Throwable.

The fallback can handle failures from the protected call, including failures that occur while the circuit is OPEN. It is not a separate retry mechanism.

Step 4: Create the Order Controller

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/{id}")
    public String getOrder(@PathVariable String id) {

        return orderService.invokeProductAPI(id);
    }
}

Now you can test the endpoint:

GET http://localhost:8081/orders/101

When Product Service is healthy and the circuit is CLOSED, the request can return product information.

When the circuit is OPEN, the protected call is rejected, and the configured fallback can return the alternative response.

7. Configure Circuit Breaker in application.properties

Now comes the important part: configuration.

Add the following properties to your application.properties file.

server.port=8081
spring.application.name=order-service

# Circuit Breaker configuration
resilience4j.circuitbreaker.instances.productService.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.productService.sliding-window-size=10
resilience4j.circuitbreaker.instances.productService.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.productService.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.productService.wait-duration-in-open-state=10s
resilience4j.circuitbreaker.instances.productService.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.productService.automatic-transition-from-open-to-half-open-enabled=true

Let’s understand each property.

7.1 sliding-window-type

resilience4j.circuitbreaker.instances.productService.sliding-window-type=COUNT_BASED

Resilience4j supports two main sliding-window types.

COUNT_BASED

Tracks a configured number of recent calls.

TIME_BASED

Tracks calls within a configured time window.

For example:

resilience4j.circuitbreaker.instances.productService.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.productService.sliding-window-size=10

This configuration uses the last 10 recorded calls in the sliding window.

Alternatively:

resilience4j.circuitbreaker.instances.productService.sliding-window-type=TIME_BASED
resilience4j.circuitbreaker.instances.productService.sliding-window-size=10

This configures a time-based window of 10 seconds.

The exact behavior depends on the selected window type and its associated configuration.

7.2 sliding-window-size

resilience4j.circuitbreaker.instances.productService.sliding-window-size=10

This specifies the window size.

  • For COUNT_BASED, it represents the number of calls.
  • For TIME_BASED, it represents the number of seconds in the window.

The Circuit Breaker uses the recorded results in that window to calculate the failure rate.

7.3 minimum-number-of-calls

resilience4j.circuitbreaker.instances.productService.minimum-number-of-calls=5

This defines the minimum number of recorded calls required before the Circuit Breaker evaluates the failure-rate threshold.

For example, if the minimum is five, the Circuit Breaker does not evaluate the failure rate until at least five calls have been recorded in the relevant window.

This helps prevent the Circuit Breaker from opening based on an extremely small sample of requests.

7.4 failure-rate-threshold

resilience4j.circuitbreaker.instances.productService.failure-rate-threshold=50

This specifies the failure-rate threshold as a percentage.

In this example, the threshold is 50%.

Suppose the relevant window contains 10 recorded calls:

  • 5 calls failed.
  • 5 calls succeeded.

The failure rate is 50%.

Once the minimum-call requirement is satisfied, reaching or exceeding the configured failure-rate threshold can cause the circuit to transition to OPEN.

The Circuit Breaker does not open merely because one request fails. It evaluates the recorded outcomes and configured thresholds.

7.5 wait-duration-in-open-state

resilience4j.circuitbreaker.instances.productService.wait-duration-in-open-state=10s

This defines how long the Circuit Breaker remains OPEN before it can transition to HALF_OPEN.

In this example, the waiting period is 10 seconds.

During that period, normal calls to the protected downstream operation are rejected by the Circuit Breaker.

7.6 permitted-number-of-calls-in-half-open-state

resilience4j.circuitbreaker.instances.productService.permitted-number-of-calls-in-half-open-state=3

This defines how many test calls are permitted while the Circuit Breaker is HALF_OPEN.

In this example, three calls are permitted.

Their recorded outcomes help determine whether the circuit should return to CLOSED or transition back to OPEN.

7.7 automatic-transition-from-open-to-half-open-enabled

resilience4j.circuitbreaker.instances.productService.automatic-transition-from-open-to-half-open-enabled=true

This enables automatic transition from OPEN to HALF_OPEN after the configured waiting period.

If automatic transition is disabled, the state transition is triggered when a later call checks the Circuit Breaker’s state after the waiting period.

The automatic option is useful when you want the state to change without waiting for another incoming request.

8. Let’s Test the Circuit Breaker

Imagine Product Service is running normally.

The Circuit Breaker starts in CLOSED.

Now, we deliberately stop Product Service to simulate an outage.

Order Service continues receiving requests.

Let’s walk through what can happen with our example configuration.

Step 1: Send requests while Product Service is unavailable

The downstream calls fail.

Resilience4j records the relevant outcomes in the sliding window.

Step 2: Reach the minimum number of calls

Our configuration requires at least five recorded calls before evaluating the failure rate.

Until the minimum is met, the Circuit Breaker does not open based on the failure-rate threshold alone.

Step 3: Reach the failure threshold

Suppose the window contains enough failed calls to reach or exceed the 50% failure-rate threshold.

The Circuit Breaker transitions from CLOSED to OPEN.

Step 4: Calls are rejected while OPEN

During the 10-second open-state waiting period, normal calls to Product Service are blocked by the Circuit Breaker.

The application can return the configured fallback response.

Step 5: Transition to HALF_OPEN

After the waiting period, the Circuit Breaker automatically transitions to HALF_OPEN because automatic transition is enabled.

Step 6: Allow test calls

The Circuit Breaker permits up to three test calls according to the configuration.

If the recorded results satisfy the success criteria, it can transition to CLOSED.

If the configured failure criteria are met, it can return to OPEN.

This is how Circuit Breaker helps protect your microservice from repeatedly calling a dependency that is failing.

9. What Happens If We Want to Record Specific Exceptions?

By default, Resilience4j Circuit Breaker records exceptions according to its configured failure rules. Its default behavior includes runtime exceptions and errors, but it is important to understand which exceptions your application actually produces.

For example, a validation exception should not necessarily count as a downstream service failure.

You can configure exceptions that should be recorded or ignored.

Example:

resilience4j.circuitbreaker.instances.productService.record-exceptions=java.io.IOException,org.springframework.web.client.HttpServerErrorException
resilience4j.circuitbreaker.instances.productService.ignore-exceptions=java.lang.IllegalArgumentException

Let’s understand this.

record-exceptions

Specifies exception types that should be recorded as failures.

ignore-exceptions

Specifies exception types that should be ignored by the Circuit Breaker when calculating the failure rate.

For example, if IllegalArgumentException represents invalid input, you may not want that failure to affect the health assessment of Product Service.

Configure exception handling carefully for the client library and version used by your application. Feign exceptions and network failures may require different handling depending on how they are represented.

10. How Does Resilience4j Change the Circuit Breaker State?

You might be wondering:

“Who actually changes the state from CLOSED to OPEN?”

Or:

“How does the Circuit Breaker automatically move from OPEN to HALF_OPEN after 10 seconds?”

Resilience4j contains the internal logic to manage these state transitions.

The Spring integration uses an aspect to intercept calls to methods annotated with @CircuitBreaker.

The Circuit Breaker then evaluates recorded call outcomes and the configured thresholds.

Conceptually, the process looks like this:

Method called
     |
     v
Circuit Breaker intercepts call
     |
     v
Check current state
     |
     +---- CLOSED ----> Allow downstream call
     |
     +---- OPEN ------> Reject protected call
     |
     +---- HALF_OPEN -> Allow limited test calls

When the configured failure threshold is exceeded in CLOSED, the Circuit Breaker can transition to OPEN.

When the OPEN waiting period expires, it can transition to HALF_OPEN.

When the test-call outcomes are evaluated, the circuit can return to CLOSED or move back to OPEN.

The framework manages these transitions, so you do not have to manually maintain the three states in your service code.

How does automatic transition work?

Your source PDF explains that Resilience4j can use a ScheduledExecutorService to schedule a transition from OPEN to HALF_OPEN.

Conceptually, a scheduled task is created with a delay.

scheduler.schedule(
    this::toHalfOpenState,
    waitDurationInMillis,
    TimeUnit.MILLISECONDS
);

This illustrates the idea of scheduling the state transition. It is a simplified example, not the complete internal implementation.

The actual scheduling and state-management logic is handled by the Resilience4j framework.

11. Circuit Breaker vs. Retry

You have already learned about Retry in the previous article.

Now let’s compare Retry and Circuit Breaker.

Imagine you bark for biscuits, but your owner doesn’t hear you.

Retry: You try again after a suitable delay because the failure might be temporary.

Circuit Breaker: If repeated failures suggest that your owner is unavailable, you stop barking for a while and check again later.

In microservices, they solve different problems.

Feature Retry Circuit Breaker
Main purpose Try a failed operation again Stop repeated calls to an unhealthy dependency
Behavior after failure May make another attempt Records outcomes and may open the circuit
Delay Can be fixed or calculated using a backoff strategy Uses an open-state waiting period
Protects against Some temporary failures Repeated calls to failing dependencies
State management Tracks retry attempts Uses CLOSED, OPEN, and HALF_OPEN states

Both patterns can be used together, but their interaction needs careful configuration.

For example, aggressive retries can generate additional failures and affect the Circuit Breaker’s recorded results.

A well-designed application should limit retry attempts and ensure that the Circuit Breaker protects the appropriate downstream operation.

12. Real-World Example: An E-Commerce Application

Imagine an e-commerce application with three microservices:

  • Product Service
  • Payment Service
  • Inventory Service

The Order Service calls all three services during the order process.

Now suppose Payment Service becomes unavailable.

Without appropriate resilience mechanisms, Order Service may continue making unsuccessful calls to Payment Service, increasing latency and consuming resources.

With a Circuit Breaker, the Payment operation can be protected separately.

                 Order Service
                       |
          +------------+------------+
          |            |            |
          v            v            v
     Product API   Payment API  Inventory API
          |            |            |
          v            v            v
     Product CB    Payment CB   Inventory CB
          |            |            |
          v            v            v
     Product       Payment      Inventory
     Service       Service      Service

Each Circuit Breaker can monitor the results of calls to its own downstream dependency.

If Payment Service fails repeatedly, its Circuit Breaker can open without automatically opening the Circuit Breakers for Product Service or Inventory Service.

This is an important advantage of isolating resilience policies by dependency.

The application still needs appropriate business handling. For example, if payment is mandatory to complete an order, a fallback should not silently pretend that payment succeeded.

13. Best Practices for Circuit Breaker

Before using Circuit Breaker in production, remember these points.

1. Choose the correct failure threshold

A threshold that is too sensitive can open the circuit unnecessarily. A threshold that is too high can allow excessive failures before the circuit opens.

2. Configure the sliding window carefully

Choose between COUNT_BASED and TIME_BASED according to your traffic patterns and monitoring requirements.

3. Set a suitable minimum number of calls

Avoid making decisions based on an extremely small number of requests.

4. Choose an appropriate open-state duration

The waiting period should give the downstream service an opportunity to recover without creating unnecessary delays.

5. Use sensible fallback behavior

Return an appropriate error or alternative response. Never report a business operation as successful if it actually failed.

6. Configure timeouts

Circuit Breaker does not replace connection timeouts or read timeouts. A slow downstream call can still occupy resources until its timeout expires.

7. Monitor the Circuit Breaker

Monitor failure rates, state transitions, rejected calls, and downstream response times.

8. Use Retry carefully

Retry and Circuit Breaker can complement each other, but retries should be bounded and limited to appropriate failures.

14. Key Takeaways 🐢

Let’s finish our dog lesson!

Imagine you’re a dog waiting for biscuits.

At first, your owner is available, so you can bark normally.

When your owner becomes unavailable, you stop barking for a while.

After some time, you try a few times to check whether your owner is available again.

If everything works, you return to normal.

That’s the basic idea behind Circuit Breaker.

Remember these five things:

  1. CLOSED: Calls are allowed, and outcomes are monitored.
  2. OPEN: Calls to the protected downstream operation are rejected.
  3. HALF_OPEN: A limited number of test calls check whether the service has recovered.
  4. Resilience4j: Provides Circuit Breaker support for Spring Boot applications.
  5. Configuration matters: Sliding windows, minimum calls, failure thresholds, and open-state waiting periods determine how the circuit behaves.

The easiest way to remember it:

Retry means trying again.

Circuit Breaker means knowing when to stop trying for a while.

A smart dog doesn’t bark forever at an owner who is sleeping. Similarly, a resilient microservice doesn’t need to keep calling a dependency that is likely to fail.

In the next article, we’ll explore TimeLimiter in Spring Boot Microservices, using the same simple dog-friendly approach.

Until then, keep coding and keep learning! πŸΆπŸ’»

Leave a Reply