The circuit breaker pattern borrows its name from the breaker in your home's electrical panel. When too much current flows, the breaker trips and cuts the circuit so the wiring does not catch fire. In microservices, the "fire" is a cascading failure, and the breaker cuts off calls to a service that has stopped responding correctly.
This is one of the most important patterns in distributed systems because of a hard truth: the moment you turn an in-process method call into a network call, that call can hang. And a hung call is more dangerous than a fast error, because it ties up the caller's resources while it waits.
Why cascading failure happens
Suppose your Order service calls your Payment service over HTTP. Payment gets slow — its database is struggling — and each call now takes 30 seconds instead of 50 milliseconds.
Order keeps accepting requests, and each one spawns a call to Payment that blocks a thread for 30 seconds. Within a minute, every thread in Order's pool is stuck waiting on Payment. Now Order stops responding too, so the services that call Order start hanging. One slow service has taken down half the system. This chain reaction is cascading failure, and it is exactly what the core microservices model exposes you to.
Pro tip: The danger is not the failing service — it is the healthy callers waiting on it. A circuit breaker protects the caller, not the callee. It gives the struggling service room to recover by taking the load off it.
The three states
A circuit breaker is a small state machine wrapped around a remote call:
- Closed — normal operation. Calls pass through, and the breaker counts failures. If the failure rate crosses a threshold (say 50% of the last 20 calls), it trips to open.
- Open — the breaker is tripped. Calls fail immediately, without touching the service. This "fail fast" is the whole point: no threads hang. After a wait period, it moves to half-open.
- Half-open — a probe. The breaker lets a few trial calls through. If they succeed, the service has recovered and the breaker closes. If they fail, it snaps back to open and waits again.
The transition from open back to half-open is what makes the pattern self-healing. You never have to manually restart anything; the breaker keeps testing and closes on its own.
Implementing it with Resilience4j
In Spring Boot the standard library is Resilience4j. You configure the breaker in
application.yml:
resilience4j:
circuitbreaker:
instances:
paymentService:
sliding-window-size: 20 # look at the last 20 calls
failure-rate-threshold: 50 # trip if 50% fail
wait-duration-in-open-state: 10s # stay open 10s before probing
permitted-number-of-calls-in-half-open-state: 3
slow-call-duration-threshold: 2s # a 2s+ call counts as a failure
slow-call-rate-threshold: 50
Then you wrap the risky call and supply a fallback:
@Service
public class PaymentClient {
private final RestClient rest;
public PaymentClient(RestClient.Builder builder) {
this.rest = builder.baseUrl("http://payment-service").build();
}
@CircuitBreaker(name = "paymentService", fallbackMethod = "queueForLater")
public PaymentResult charge(PaymentRequest request) {
return rest.post()
.uri("/payments")
.body(request)
.retrieve()
.body(PaymentResult.class);
}
// Called when the breaker is open or the call fails
private PaymentResult queueForLater(PaymentRequest request, Throwable cause) {
// Degrade gracefully: accept the order, settle payment asynchronously
outbox.save(request);
return PaymentResult.pending(request.orderId());
}
}
When Payment is healthy, charge runs normally. When it starts failing, the breaker trips and
every subsequent call jumps straight to queueForLater without waiting on a dead service.
Instead of an error, the customer's order is accepted and settled later — a degraded but
working experience.
The fallback is where the design happens
A circuit breaker without a thoughtful fallback just turns slow failures into fast failures. That is better, but the real win is a fallback that keeps the feature usable:
- Return cached or last-known data for a read (show yesterday's prices).
- Queue the work and process it when the service recovers, as above.
- Return a sensible default (an empty recommendations list, not a crash).
- Show a clear "temporarily unavailable" message for things that genuinely cannot degrade.
Choosing the right fallback is a business decision, not a technical one. "Can we accept the order now and charge later?" is a question for the product owner, not just the developer.
How it fits with timeouts and retries
A common interview trap is confusing these three. They work together:
- A timeout caps how long one call can hang. Without it, the breaker has nothing to measure and calls can block forever.
- A retry re-attempts a call that failed for a transient reason, like a brief network blip.
- The circuit breaker cuts off calls entirely once failures are frequent, so you stop hammering a service that is genuinely down.
The dangerous combination is retries without a breaker: when a service is overloaded, retries triple the traffic and finish it off. Wrap retries in a breaker so the retries stop once the failure pattern is clear.
Common mistake: Setting the failure threshold so high or the window so large that the breaker never trips in time. If it takes 500 failures to open, cascading failure has already spread. Tune the window and threshold against real traffic, and watch the breaker's state transitions in your monitoring.
The bulkhead: contain the damage further
The circuit breaker is usually paired with a second pattern called the bulkhead, named after the watertight compartments in a ship's hull. If one compartment floods, the bulkheads stop the water from sinking the whole vessel.
Applied to software, a bulkhead isolates resources so that trouble in one dependency cannot drain the resources every other dependency needs. Concretely, you give calls to the Payment service their own bounded thread pool or connection pool, separate from calls to the Catalog service. If Payment slows down and saturates its pool, Catalog calls keep flowing through their own untouched pool. Without bulkheads, a single slow dependency can exhaust one shared pool and starve everything.
Resilience4j supports this directly. The mental model is simple: the circuit breaker decides whether to call, and the bulkhead limits how many concurrent calls a dependency can consume. Together they stop both the "keep calling a dead service" failure and the "one hog starves everyone" failure.
Tuning against real traffic
The hardest part of a circuit breaker is not the code — it is the numbers. A window that is too small trips on noise; too large and it reacts too late. A wait-in-open duration that is too short hammers a recovering service before it is ready; too long and you stay degraded after recovery.
There are no universal values. Start from your actual traffic: how many calls per second does this dependency get, what is its normal latency, and how long does it typically take to recover from a blip? Set the slow-call threshold just above normal latency, size the window to cover a few seconds of real traffic, and then watch the breaker in production and adjust. Treat these as living settings, not constants you set once and forget.
You cannot tune what you cannot see
A circuit breaker is only as good as your visibility into it. You need to know how often it trips, for which service, and whether fallbacks are firing. That is why resilience and observability are studied together — breaker state changes should be metrics on a dashboard and alerts, not surprises you discover from angry users. Pair this with clean REST exception handling so the errors that reach clients are deliberate, not accidental stack traces.
Interview relevance
Expect "how do you prevent one slow service from taking down the whole system?" The winning answer walks through cascading failure, names the closed-open-half-open states, and stresses that the breaker protects the caller and needs a real fallback. If you can add that a breaker without a timeout is meaningless and that retries without a breaker make overload worse, you sound like someone who has run this in production, not just read about it.
Not every call needs a breaker
A final piece of honest guidance: do not wrap every method in a circuit breaker reflexively. The pattern only makes sense around calls that cross a process boundary — a network call to another service, a database, or an external API. Wrapping a plain in-memory computation in a breaker adds overhead and configuration for a failure mode that cannot happen. Reserve breakers for remote dependencies that can genuinely be slow or unavailable, and leave local, fast, deterministic code alone. Applied where it belongs, the breaker is one of the highest-leverage tools you have; sprayed everywhere, it is just noise.
Related concepts
The circuit breaker is the entry point to resilience engineering. From here, look at where it lives in the request path via the API gateway, and study the saga pattern for how to keep data consistent when a call in the middle of a workflow fails and your fallback kicks in.
Frequently Asked Questions
What problem does the circuit breaker pattern solve?
What are the three states of a circuit breaker?
What is the difference between a circuit breaker and a retry?
What is a fallback in the circuit breaker pattern?
Does a circuit breaker replace timeouts?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

