DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

I Built a Resilience4j Lab: Watch a Circuit Breaker Trip (+ Retry With Backoff)

A circuit breaker is easy to describe — "it stops calling a failing service" — and surprisingly hard to picture. When does it trip? What's HALF_OPEN actually for? Why does failing fast help? So I built a lab that runs the real Resilience4j state machine on a sliding window.

▶ Live demo: https://dev48v.github.io/resilience4j-lab/
Source (single file, zero deps): https://github.com/dev48v/resilience4j-lab

The state machine, live

Flip "downstream is failing" on and click call. Each result lands in a sliding window; once the failure rate crosses the threshold, the breaker trips through three states:

  • CLOSED — calls pass through; the breaker just tracks the failure rate.
  • OPEN — the rate hit 50%, so calls now fail instantly with CallNotPermittedException for waitDurationInOpenState. Nothing piles up waiting on a dead dependency — that's the whole value. A slow/dead downstream can otherwise exhaust your thread pool and take your service down with it.
  • HALF_OPEN — after the wait, a few trial calls are allowed through. Succeed → back to CLOSED (recovered). One fails → straight back to OPEN.

The config mirrors real Resilience4j:

CircuitBreakerConfig.custom()
    .failureRateThreshold(50)                       // %
    .slidingWindowSize(10)
    .waitDurationInOpenState(Duration.ofSeconds(5))
    .permittedNumberOfCallsInHalfOpenState(3)
    .build();
Enter fullscreen mode Exit fullscreen mode
@CircuitBreaker(name = "downstream", fallbackMethod = "fallback")
public String call() { return downstream.get(); }
Enter fullscreen mode Exit fullscreen mode

Watching the sliding window fill with red and the breaker flip to OPEN at exactly the threshold makes the whole thing concrete in a way the docs don't.

Retry (and why order matters)

The Retry tab shows @Retry re-invoking a flaky call with exponential backoff — 500ms → 1s → 2s — so a brief blip is absorbed transparently. Push "fails before success" past maxAttempts and every attempt is exhausted before the exception propagates.

The gotcha the lab sets up: nest Retry inside the CircuitBreaker, not the other way round. Otherwise your retries keep hammering a service that's already down — the breaker should get the final say and fail fast. (Spring applies them by @Order; CircuitBreaker outermost.)

One index.html, works offline. If it made circuit breakers click, a star helps others find it: https://github.com/dev48v/resilience4j-lab

Top comments (0)