MediumBuilding Resilient Microservices: The Complete Guide to Circuit Breaker Pattern in Spring Boot | by Lohith V P | Oct, 2026 | Medium
Sitemap

Building Resilient Microservices: The Complete Guide to Circuit Breaker Pattern in Spring Boot

7 min readOct 20, 2026

--

Introduction

Imagine your e-commerce application depends on a payment service. One day, that payment service starts responding slowly or fails completely. Without protection, your application keeps trying to call it, threads pile up waiting for timeouts, resources exhaust, and suddenly your entire application crashes. This is the cascading failure problem that plagues microservices architectures.

The Circuit Breaker pattern solves this elegantly. Like an electrical circuit breaker that trips to prevent damage, a software circuit breaker detects failures and temporarily stops calling the failing service, giving it time to recover while your application gracefully degrades.

In this comprehensive guide, you’ll learn:

  • How Circuit Breaker pattern works
  • The three states: CLOSED, OPEN, and HALF-OPEN
  • Complete Spring Boot implementation with Resilience4j
  • Configuration best practices
  • Real-world production examples

Let’s dive in!

Understanding Circuit Breaker States

The Circuit Breaker pattern operates as a finite state machine with three primary states:

1. CLOSED State -Normal Operation

In the CLOSED state, everything works normally:

  • All requests pass through to the target service
  • The circuit breaker monitors success and failure rates
  • If failure rate stays below threshold (typically 50%), state remains CLOSED

When does it transition to OPEN?

  • Failure rate ≥ 50% of last N calls (configurable)
  • Slow call rate ≥ threshold (calls taking too long)
  • Minimum number of calls reached before evaluation

2. OPEN State — Protection Mode

When failures exceed the threshold, the circuit “trips” to OPEN:

  • All requests are immediately rejected without calling the target service
  • Fallback method executes instead
  • Prevents further strain on the failing service
  • System waits for waitDurationInOpenState (typically 30-60 seconds)

Why is this important? It gives the failing service time to recover without being hammered by continued requests.

3. HALF-OPEN State — Testing Recovery

After the wait duration expires:

  • Circuit transitions to HALF-OPEN
  • Limited number of test requests allowed through
  • If test requests succeed → Circuit closes (back to CLOSED)
  • If test requests fail → Circuit reopens (back to OPEN)

This is the critical diagnostic phase where the circuit actively tests whether the service has recovered.

Press enter or click to view image in full size

Architecture Overview

Here’s how Circuit Breaker fits into your Spring Boot microservices architecture:

Flow:

  1. Client sends request to /api/orders
  2. Request passes through Circuit Breaker
  3. If CLOSED: Request forwarded to Payment API
  4. If OPEN: Request blocked, fallback method executes
  5. Response returned to client (either from service or fallback)

Complete Spring Boot Implementation

Step 1: Add Dependencies

For Spring Boot 3.x (Jakarta EE):

<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Resilience4j Circuit Breaker -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<version>3.0.0</version>
</dependency>

<!-- AOP for annotations -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- Actuator for monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Lombok (optional) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

Step 2: Configure Circuit Breaker

Get Lohith V P’s stories in your inbox

Join Medium for free to get updates from this writer.

In application.yml:

resilience4j:
circuitbreaker:
instances:
paymentService:
# Sliding window: evaluate based on last 10 calls
slidingWindowSize: 10
slidingWindowType: COUNT_BASED

# Don't calculate failure rate until at least 5 calls recorded
minimumNumberOfCalls: 5

# Trip circuit when 50%+ of calls fail
failureRateThreshold: 50

# Also trip when 75%+ of calls are slow
slowCallRateThreshold: 75

# Calls taking longer than 5 seconds are "slow"
slowCallDurationThreshold: 5000ms

# How long to stay OPEN before testing recovery
waitDurationInOpenState: 30s

# Number of test requests in HALF-OPEN state
permittedNumberOfCallsInHalfOpenState: 5

# Automatically transition from OPEN to HALF-OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true

# Ignore these exceptions (don't count as failures)
ignoreExceptions:
- com.example.exceptions.BusinessException

Step 3: Create the Service

@Service
@Slf4j
public class PaymentService {

private final RestTemplate restTemplate;

public PaymentService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

@CircuitBreaker(
name = "paymentService",
fallbackMethod = "paymentFallback"
)
@Retry(
name = "paymentService",
fallbackMethod = "paymentFallback"
)
public PaymentResponse processPayment(PaymentRequest request) {
log.info("Processing payment for order: {}", request.getOrderId());

// Call external payment API
return restTemplate.postForObject(
"http://payment-api/api/payments",
request,
PaymentResponse.class
);
}

// Fallback method - MUST have same parameters + Throwable
public PaymentResponse paymentFallback(PaymentRequest request, Throwable t) {
log.warn("Payment service failed, using fallback. Error: {}", t.getMessage());

// Return cached response, default value, or graceful error
return PaymentResponse.builder()
.status("PENDING")
.message("Payment processing delayed. Please check back later.")
.transactionId(null)
.build();
}
}

Key Points:

  • @CircuitBreaker annotation wraps the method
  • name matches the configuration in application.yml
  • fallbackMethod specifies the fallback handler
  • Fallback method signature: same parameters + Throwable as last parameter
  • Can combine with @Retry for additional resilience

Step 4: Create the Controller

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

private final PaymentService paymentService;

public OrderController(PaymentService paymentService) {
this.paymentService = paymentService;
}

@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
// Process payment with circuit breaker protection
PaymentResponse payment = paymentService.processPayment(
PaymentRequest.builder()
.orderId(request.getOrderId())
.amount(request.getAmount())
.build()
);

// Continue with order creation
OrderResponse response = createOrderInDatabase(request, payment);

return ResponseEntity.ok(response);
}
}

Step 5: Monitor Circuit Breaker State

Add to application.yml

management:
endpoints:
web:
exposure:
include: health,metrics,circuitbreakers,circuitbreakerevents
endpoint:
health:
show-details: always

Check circuit breaker state

# Get all circuit breakers
curl http://localhost:8080/actuator/circuitbreakers
# Get specific circuit breaker state
curl http://localhost:8080/actuator/circuitbreakers/paymentService
# Get circuit breaker events
curl http://localhost:8080/actuator/circuitbreakerevents/paymentService

Sample Response:

{
"state": "CLOSED",
"failureRate": "15.0%",
"slowCallRate": "5.0%",
"numberOfBufferedCalls": 20,
"numberOfFailedCalls": 3,
"numberOfSlowCalls": 1
}

Configuration Parameters Explained

1. failureRateThreshold

  • Default Value: 50%
  • Description: Percentage of failed calls required to trip the circuit breaker.
  • When to Adjust: Lower to 40% for critical services that need faster failure detection. Increase to 60% for stable services that can tolerate occasional failures.

2. slowCallRateThreshold

  • Default Value: 100%
  • Description: Percentage of slow calls required to trip the circuit breaker.
  • When to Adjust: Reduce to 75–80% if slow responses indicate potential service issues.

3. slowCallDurationThreshold

  • Default Value: 60 seconds
  • Description: Duration beyond which a call is considered slow.
  • When to Adjust: Set according to your SLA. For example, use 3 seconds for APIs that require fast responses.

4. waitDurationInOpenState

  • Default Value: 60 seconds
  • Description: Time the circuit breaker remains in the OPEN state before transitioning to HALF-OPEN.
  • When to Adjust: Reduce to 10–30 seconds for services that recover quickly.

5. slidingWindowSize

  • Default Value: 100 calls
  • Description: Number of calls considered when calculating failure and slow-call rates.
  • When to Adjust: Use 10–20 calls for faster detection or a larger window for more stable rate calculations

6. minimumNumberOfCalls

  • Default Value: 100 calls
  • Description: Minimum number of calls required before the circuit breaker calculates failure and slow-call rates.
  • When to Adjust: Lower to 5–10 calls for low-traffic services. Keep in mind that smaller samples can produce less reliable results.

7. permittedNumberOfCallsInHalfOpenState

  • Default Value: 10 calls
  • Description: Number of test calls allowed in the HALF-OPEN state to check whether the service has recovered.
  • When to Adjust: Reduce to 3–5 calls to avoid overwhelming a recovering service.

Production Best Practices

1. Combine with Retry Pattern

@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
@Retry(name = "paymentService", fallbackMethod = "fallback")
@TimeLimiter(name = "paymentService")
public PaymentResponse processPayment(PaymentRequest request) {
// Your code
}

Retry configuration

resilience4j:
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 1s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2

2. Add Timeout Protection

resilience4j:
timelimiter:
instances:
paymentService:
timeoutDuration: 3s
cancelRunningFuture: true

3. Implement Meaningful Fallbacks

Bad fallback:

public PaymentResponse fallback(PaymentRequest request, Throwable t) {
throw new RuntimeException("Service unavailable");
}

Good fallback:

public PaymentResponse fallback(PaymentRequest request, Throwable t) {
// Option 1: Return cached data
return cache.get(request.getUserId());

// Option 2: Return default/graceful response
return PaymentResponse.pending("Processing delayed");

// Option 3: Queue for later processing
retryQueue.add(request);
return PaymentResponse.queued("Will process shortly");
}

4. Monitor and Alert

@Component
public class CircuitBreakerMonitor {

private final CircuitBreakerRegistry circuitBreakerRegistry;

public CircuitBreakerMonitor(CircuitBreakerRegistry registry) {
this.circuitBreakerRegistry = registry;
}

@Scheduled(fixedRate = 60000) // Every minute
public void checkCircuitBreakers() {
circuitBreakerRegistry.getAllCircuitBreakers().forEach(cb -> {
CircuitBreaker.Metrics metrics = cb.getMetrics();

if (cb.getState() == CircuitBreaker.State.OPEN) {
log.warn("Circuit breaker {} is OPEN! Failure rate: {}%",
cb.getName(),
metrics.getFailureRate());

// Send alert to Slack/PagerDuty
alertService.sendAlert(cb.getName(), metrics);
}
});
}
}

5. Test Circuit Breaker Behavior

@SpringBootTest
class PaymentServiceTest {

@Autowired
private PaymentService paymentService;

@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;

@Test
void shouldTripCircuitBreakerAfterThresholdFailures() {
// Make 5 failing calls (minimumNumberOfCalls)
for (int i = 0; i < 5; i++) {
try {
paymentService.processPayment(testRequest);
} catch (Exception e) {
// Expected
}
}

// Verify circuit breaker is OPEN
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("paymentService");
assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.OPEN);
}

@Test
void shouldUseFallbackWhenCircuitOpen() {
// Force circuit to OPEN
circuitBreakerRegistry.circuitBreaker("paymentService")
.transitionToOpenState();

// Call should use fallback
PaymentResponse response = paymentService.processPayment(testRequest);

assertThat(response.getStatus()).isEqualTo("PENDING");
assertThat(response.getMessage()).contains("delayed");
}
}

Real-World Example: E-Commerce Order Processing

@Service
public class OrderProcessingService {

@CircuitBreaker(name = "inventoryService", fallbackMethod = "checkInventoryFallback")
public boolean checkInventory(String productId, int quantity) {
return restTemplate.getForObject(
"http://inventory-service/api/stock/{id}/{qty}",
Boolean.class,
productId,
quantity
);
}

@CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
return restTemplate.postForObject(
"http://payment-service/api/pay",
request,
PaymentResponse.class
);
}

@CircuitBreaker(name = "notificationService", fallbackMethod = "notifyFallback")
public void sendOrderConfirmation(String email, Order order) {
restTemplate.postForObject(
"http://notification-service/api/email",
new EmailRequest(email, order),
Void.class
);
}

// Fallback methods
public boolean checkInventoryFallback(String productId, int quantity, Throwable t) {
log.warn("Inventory service unavailable, assuming stock available");
return true; // Assume in stock to avoid blocking orders
}

public PaymentResponse processPaymentFallback(PaymentRequest request, Throwable t) {
log.error("Payment service down, queuing for retry");
paymentRetryQueue.add(request);
return PaymentResponse.queued("Payment will be processed shortly");
}

public void notifyFallback(String email, Order order, Throwable t) {
log.warn("Failed to send email to {}, will retry later", email);
emailRetryQueue.add(new EmailRequest(email, order));
}
}

Common Pitfalls to Avoid

❌ Pitfall 1: Too Aggressive Circuit Breaking

# BAD: Trips too easily
failureRateThreshold: 20
minimumNumberOfCalls: 2
waitDurationInOpenState: 5s

Result: Circuit trips on normal transient failures, causing unnecessary fallbacks.

Solution: Use reasonable thresholds based on your SLA.

❌ Pitfall 2: No Fallback or Poor Fallback

// BAD: Just rethrows exception
public PaymentResponse fallback(PaymentRequest request, Throwable t) {
throw new ServiceException("Payment failed");
}

Result: Users see errors instead of graceful degradation.

Solution: Return meaningful fallback responses.

❌ Pitfall 3: Ignoring Slow Calls

# BAD: Only tracks failures
slowCallRateThreshold: 100
slowCallDurationThreshold: 60000ms

Result: Service degrades but circuit never trips because calls “succeed” (just very slowly).

Solution: Set appropriate slow call thresholds.

❌ Pitfall 4: No Monitoring

Result: Circuit trips but nobody knows until users complain.

Solution: Implement monitoring and alerting.

Performance Impact

Circuit Breaker adds minimal overhead:

  • Memory: ~1–5 MB per circuit breaker instance
  • CPU: <1% additional CPU usage
  • Latency: <1ms per call for state checking

Trade-off: Small overhead for massive resilience improvement.

Conclusion

The Circuit Breaker pattern is essential for building resilient microservices in Spring Boot. By implementing it with Resilience4j, you get:

✅ Automatic failure detection and protection
✅ Graceful degradation instead of catastrophic failures
✅ Self-healing through HALF-OPEN state testing
✅ Production visibility via Actuator endpoints
✅ Minimal code changes with annotations

Key Takeaways:

  1. Start with conservative thresholds (50% failure rate, 30s wait)
  2. Always implement meaningful fallbacks
  3. Monitor circuit breaker states in production
  4. Combine with Retry and Timeout patterns
  5. Test failure scenarios before deploying

--

--

Lohith V P
Lohith V P