Introduction
Microservices involve multiple services working together. Sometimes, a service might fail, and repeated calls to it can cause bigger problems. The Circuit Breaker Pattern helps stop calls to a failing service and gives it time to recover. It makes systems more stable and reliable by preventing overloading, reducing delays, and optimizing resource usage. However, to ensure that the circuit breaker is functioning as expected, it is important to validate its behavior using various testing approaches.
Problem Statement
Microservices systems often face:
- Overloading: Failing services can be overwhelmed by repeated requests.
- Delays: Slow responses can make the whole system seem unresponsive.
- Resource Waste: Retrying failed calls can waste resources like CPU and memory.
- Cascading Failures: When one service fails, it can affect multiple dependent services.
Solution: Circuit Breaker Pattern
The Circuit Breaker pattern addresses these challenges by monitoring failures and temporarily halting requests to a failing service until it recovers.
How Circuit Breakers Work:
- Normal State (Closed): Calls go to the service as usual.
- Open State: Calls are stopped if the service is failing.
- Half-Open State: A few test calls are made to check if the service has recovered.
By using circuit breakers, services can prevent cascading failures, improve system stability, and allow faster recovery.
A full working example is available here
1. Service Setup (Spring Boot Example)
PaymentServiceApplication.java (Main Class)
@SpringBootApplication public class PaymentServiceApplication { public static void main(String[] args) { SpringApplication.run(PaymentServiceApplication.class, args); } }
PaymentController.java (Service Controller)
@RestController public class PaymentController { private final PaymentService paymentService; public PaymentController(PaymentService paymentService) { this.paymentService = paymentService; } @GetMapping("/process-payment") public String processPayment(@RequestParam double amount) { return paymentService.makePayment(amount); } }
2. Implementing Circuit Breaker
PaymentService.java (Circuit Breaker Implementation)
@Service public class PaymentService { private final RestTemplate restTemplate; private final CircuitBreaker circuitBreaker; public PaymentService() { this.restTemplate = new RestTemplate(); CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) // Open circuit if 50% failures occur .waitDurationInOpenState(Duration.ofSeconds(5)) // Wait 5 seconds before retry .slidingWindowSize(10) // Evaluate last 10 calls .build(); this.circuitBreaker = CircuitBreaker.of("paymentServiceCB", config); } public String makePayment(double amount) { Supplier<String> paymentSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, () -> callExternalPaymentApi(amount)); return paymentSupplier.get(); } private String callExternalPaymentApi(double amount) { String paymentServiceUrl = "http://localhost:8080/external-payment?amount=" + amount; String response = restTemplate.getForObject(paymentServiceUrl, String.class); if (amount > 1000) { throw new RuntimeException("Transaction limit exceeded"); } return response; } }
Validating Circuit Breaker Functionality
Example of Unit Testing
PaymentServiceTest.java
@SpringBootTest public class PaymentServiceTest { @Autowired private PaymentService paymentService; @Test public void testCircuitBreakerOpensOnFailures() { for (int i = 0; i < 10; i++) { try { paymentService.makePayment(1500); } catch (Exception e) { System.out.println("Expected Failure: " + e.getMessage()); } } assertEquals(CircuitBreaker.State.OPEN, paymentService.getCircuitBreakerState()); } }
Example of Integration Testing
PaymentControllerTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class PaymentControllerTest { @Autowired private MockMvc mockMvc; @Test public void testProcessPaymentEndpoint() throws Exception { mockMvc.perform(get("/process-payment?amount=500")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Payment Processed Successfully"))); } }
Example of Functional Testing (Using Postman)
- Open Postman and make a
GET
request to:
http://localhost:8080/process-payment
2. Observe the response for successful or fallback messages.
3. Use Postman’s Runner feature to simulate multiple requests and verify circuit breaker behavior.
Example of Load Testing (Using Gatling)
Example of Gatling Script
import io.gatling.core.scenario.Simulation import io.gatling.core.Predef._ import io.gatling.http.Predef._ class CircuitBreakerSimulation extends Simulation { val httpProtocol = http.baseUrl("http://localhost:8080") val scn = scenario("CircuitBreakerTest") .exec(http("Process Payment") .get("/process-payment?amount=500") .check(status.is(200))) setUp( scn.inject(atOnceUsers(100)) ).protocols(httpProtocol) }
Run the above script using Gatling and analyze the generated reports to verify circuit breaker behavior.
Chaos Engineering with Chaos Monkey
Step 1: Add Chaos Monkey Dependency
Add the following dependency in pom.xml
:
<dependency> <groupId>de.codecentric</groupId> <artifactId>chaos-monkey-spring-boot</artifactId> <version>2.5.0</version> </dependency>
Step 2: Configure Chaos Monkey
Add the following to application.properties
:
management.endpoints.web.exposure.include=* management.endpoint.chaosmonkey.enabled=true chaos.monkey.enabled=true chaos.monkey.assaults.latency-active=true chaos.monkey.assaults.latency-range-start=1000 chaos.monkey.assaults.latency-range-end=3000 chaos.monkey.assaults.exceptions-active=true chaos.monkey.assaults.exception-rate=50 chaos.monkey.watcher.controller=true chaos.monkey.watcher.service=true
Step 3: Running Chaos Monkey
Start the application:
mvn spring-boot:run
Enable Chaos Monkey attack via:
curl -X POST "http://localhost:8080/actuator/chaosmonkey/enable"
Observe latency and fallback responses.
Step 4: Monitoring Chaos Impact
Access actuator metrics:
curl "http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker"
Conclusion
The Circuit Breaker Pattern plays a crucial role in ensuring system stability by preventing repeated calls to a failing service. By implementing robust testing strategies right from the development face is critical in ensuring the reliability of the system Unit, Integration and using tools like Postman, Gatling, Resilience4j, and Chaos Monkey, we can verify that the circuit breaker is functioning correctly, ensuring improved system reliability and performance under failure conditions.
Publication & Disclaimer : This article is published on both [My Blog] and [Medium]. The views expressed in this article are my own and do not reflect the views of my employer.