An outage rarely starts with one big crash; it starts with one slow dependency and a pile of waiting threads.
Question: What is a circuit breaker in Spring Boot?
Answer: A circuit breaker is a safety pattern that stops your app from calling a failing remote service over and over. In Spring Boot, we usually add it with Resilience4j, which watches recent failures and changes state from closed to open and then to half-open. When it opens, the app fails fast or uses a fallback so the whole system stays healthy.
Interview-Ready Answer: In Spring Boot, I use a circuit breaker to protect the app from a bad downstream service. It tracks failures in a sliding window, opens when the failure rate crosses the threshold, and then later allows a few test calls in half-open state. The key benefit is that it fails fast instead of tying up threads, and with Resilience4j I can also return a safe fallback response.
A circuit breaker is a resilience tool for remote calls. Think of it like an automatic fuse box: if one line keeps sparking, the breaker cuts the power so the whole building does not burn down. In microservices, the "sparks" are repeated timeouts, 5xx responses, or other call failures from another service.
@CircuitBreaker.closed, the call goes through and Resilience4j records whether it succeeded, failed, or was slow.open and short-circuits new calls with CallNotPermittedException instead of hitting the downstream service.half-open and allows a small number of test calls.Important default numbers: common Resilience4j defaults are a 100-call sliding window, a 50% failure-rate threshold, 10 half-open test calls, and a 60-second wait in open state. For demos and tests, people usually shrink the window to 5 or 10 calls so the breaker trips quickly.
Use a circuit breaker around slow or unstable remote dependencies: payment gateways, inventory services, third-party APIs, or even a fragile database call through another service. It is most valuable when waiting longer is worse than failing fast. That is why it is different from retry: retry says "try again"; circuit breaker says "stop trying for a while".
| Technique | What it does | Best use | Trade-off |
|---|---|---|---|
| Circuit Breaker | Stops calls after failures | Unstable remote calls | Fast failover |
| Retry | Tries again | Temporary glitches | Can add load |
| Timeout | Limits waiting time | Slow responses | No future protection |
| Bulkhead | Limits concurrency | Shared-resource protection | Lower throughput |
The runtime overhead is small. The decision is effectively constant time per call, and memory use is proportional to the window size, so the bookkeeping is O(1) per call with O(N) space for a count-based window of size N. The real tuning knobs are the failure threshold, the minimum number of calls before evaluation, the wait duration in open state, and the slow-call threshold. A common beginner mistake is using the default window in a test and wondering why the breaker never opens soon enough.
Imagine a checkout service calling a payment gateway during a flash sale. For the first minute everything is fine, then the gateway starts taking 8 seconds per request. Without a circuit breaker, Tomcat threads pile up waiting, p99 latency jumps, and users start seeing 504s even for unrelated requests because the server is exhausted.
With a circuit breaker, the checkout service stops hammering the gateway after the failure threshold is reached. It returns a controlled message like "payment temporarily unavailable, please retry" and logs the failure cleanly. That is much better than letting the whole site freeze.
What goes wrong when people misunderstand it: a team treats retry as the whole solution and keeps calling the broken gateway five times per request. The outage gets louder: more traffic, more timeouts, more thread starvation, and sometimes more 5xx responses than before. In logs you often see timeouts first, then CallNotPermittedException once the breaker finally opens.
package com.example.circuitbreaker;\n\nimport io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;\nimport io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.web.client.RestTemplateBuilder;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.client.RestTemplate;\n\nimport java.time.Duration;\n\n@SpringBootApplication\npublic class CircuitBreakerDemoApplication {\n public static void main(String[] args) {\n SpringApplication.run(CircuitBreakerDemoApplication.class, args);\n }\n\n @Bean\n RestTemplate restTemplate(RestTemplateBuilder builder) {\n // Short timeouts help the breaker get a quick signal when the downstream is unhealthy.\n return builder\n .setConnectTimeout(Duration.ofSeconds(1))\n .setReadTimeout(Duration.ofSeconds(2))\n .build();\n }\n}\n\n@RestController\n@RequestMapping("/inventory")\nclass InventoryController {\n\n @GetMapping("/{sku}")\n ResponseEntity<String> inventory(@PathVariable String sku,\n @RequestParam(defaultValue = "false") boolean fail) {\n // This endpoint simulates a remote dependency that sometimes fails.\n if (fail || "FAIL".equalsIgnoreCase(sku)) {\n return ResponseEntity.status(503).body("Inventory service unavailable for sku=" + sku);\n }\n return ResponseEntity.ok("inventory-ok for sku=" + sku);\n }\n}\n\n@RestController\n@RequestMapping("/orders")\nclass OrderController {\n private final OrderService orderService;\n\n OrderController(OrderService orderService) {\n this.orderService = orderService;\n }\n\n @GetMapping("/{sku}")\n String placeOrder(@PathVariable String sku,\n @RequestParam(defaultValue = "false") boolean failInventory) {\n return orderService.placeOrder(sku, failInventory);\n }\n\n @GetMapping("/breaker-state")\n String breakerState() {\n return orderService.breakerState();\n }\n}\n\n@Service\nclass OrderService {\n private final RestTemplate restTemplate;\n private final CircuitBreakerRegistry circuitBreakerRegistry;\n\n OrderService(RestTemplate restTemplate, CircuitBreakerRegistry circuitBreakerRegistry) {\n this.restTemplate = restTemplate;\n this.circuitBreakerRegistry = circuitBreakerRegistry;\n }\n\n @CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")\n public String placeOrder(String sku, boolean failInventory) {\n // In a real system this would call another microservice. Here we call a local endpoint to keep the demo runnable.\n String url = "http://localhost:8080/inventory/" + sku + "?fail=" + failInventory;\n ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);\n\n // RestTemplate throws for 4xx/5xx, so a 503 becomes a failure signal for the breaker.\n return "Order accepted. " + response.getBody();\n }\n\n // The fallback must match the original arguments, plus the Throwable that explains why the call failed.\n public String inventoryFallback(String sku, boolean failInventory, Throwable ex) {\n return "Order accepted with delayed inventory check for sku=" + sku +\n ". Fallback reason=" + ex.getClass().getSimpleName();\n }\n\n String breakerState() {\n return circuitBreakerRegistry.circuitBreaker("inventoryService").getState().name();\n }\n}\nFollow-up & Tricky Questions:
Common Mistakes:
Memory Hook: The breaker is the nightclub bouncer: closed means everyone gets in, open means nobody gets in, and half-open means a few people are tested at the door before the crowd is let back in.
Cheat Sheet:
Practice Tasks:
failInventory=true and watch the fallback response.CLOSED to OPEN, then check /orders/breaker-state.