Circuit Breaker

Three States

A circuit breaker sits between the caller and the remote service and watches results, like the electrical breaker in your home:

failure rate >= threshold
   +--------+  (in the last N calls)   +--------+
   | CLOSED | -----------------------> |  OPEN  |  calls are rejected at once
   +--------+                          +--------+  (fallback, no network call)
       ^                                  |   ^
       | trial calls succeed              |   | a trial call fails
       |                     wait time    v   |
       |                     passes   +-----------+
       +----------------------------- | HALF_OPEN |  let a few trial calls through
                                      +-----------+
  • CLOSED: normal. Calls go through; results are recorded in a sliding window.
  • OPEN: too many failures. Calls fail fast with CallNotPermittedException → fallback. The sick service gets time to recover.
  • HALF_OPEN: after the wait duration, a few trial calls are allowed. Success → CLOSED, failure → OPEN again.

A Circuit Breaker in Plain Java

This state machine uses a count-based window of 5 calls, opens at 50% failures, waits 10 s in OPEN and needs 2 successful trial calls to close. product-service is down between t=2 s and t=22 s. A simulated clock keeps the output deterministic:

Example

import java.util.*;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        CircuitBreaker cb = new CircuitBreaker(5, 50.0, 10_000, 2);
        long[] callTimes = {0, 1000, 2000, 3000, 4000, 5000, 8000, 14000, 20000, 24000, 25000, 26000};

        for (long t : callTimes) {
            Supplier<String> productService = () -> {
                if (t >= 2000 && t < 22000) throw new RuntimeException("503");
                return "boAt Rockerz 450";
            };
            String result = cb.call(t, productService, () -> "cached product");
            System.out.printf("t=%5dms  %-36s state=%s%n", t, result, cb.state());
        }
    }
}

enum State { CLOSED, OPEN, HALF_OPEN }

class CircuitBreaker {
    private final int windowSize;
    private final double failureRateThreshold;
    private final long waitInOpenMs;
    private final int trialCalls;
    private final Deque<Boolean> window = new ArrayDeque<>(); // true = failed
    private State state = State.CLOSED;
    private long openedAt;
    private int trialSuccesses;

    CircuitBreaker(int windowSize, double threshold, long waitInOpenMs, int trialCalls) {
        this.windowSize = windowSize;
        this.failureRateThreshold = threshold;
        this.waitInOpenMs = waitInOpenMs;
        this.trialCalls = trialCalls;
    }

    State state() { return state; }

    String call(long now, Supplier<String> remote, Supplier<String> fallback) {
        if (state == State.OPEN) {
            if (now - openedAt < waitInOpenMs) return "REJECTED -> " + fallback.get();
            state = State.HALF_OPEN;
            trialSuccesses = 0;
        }
        try {
            String value = remote.get();
            onSuccess();
            return "OK -> " + value;
        } catch (RuntimeException e) {
            onFailure(now);
            return "FAILED(" + e.getMessage() + ") -> " + fallback.get();
        }
    }

    private void onSuccess() {
        if (state == State.HALF_OPEN) {
            if (++trialSuccesses >= trialCalls) { state = State.CLOSED; window.clear(); }
        } else {
            record(false);
        }
    }

    private void onFailure(long now) {
        if (state == State.HALF_OPEN) { open(now); return; }
        record(true);
        long failed = window.stream().filter(f -> f).count();
        if (window.size() == windowSize && failed * 100.0 / windowSize >= failureRateThreshold) open(now);
    }

    private void record(boolean failed) {
        window.addLast(failed);
        if (window.size() > windowSize) window.removeFirst();
    }

    private void open(long now) { state = State.OPEN; openedAt = now; }
}

Output

t=    0ms  OK -> boAt Rockerz 450               state=CLOSED
t= 1000ms  OK -> boAt Rockerz 450               state=CLOSED
t= 2000ms  FAILED(503) -> cached product        state=CLOSED
t= 3000ms  FAILED(503) -> cached product        state=CLOSED
t= 4000ms  FAILED(503) -> cached product        state=OPEN
t= 5000ms  REJECTED -> cached product           state=OPEN
t= 8000ms  REJECTED -> cached product           state=OPEN
t=14000ms  FAILED(503) -> cached product        state=OPEN
t=20000ms  REJECTED -> cached product           state=OPEN
t=24000ms  OK -> boAt Rockerz 450               state=HALF_OPEN
t=25000ms  OK -> boAt Rockerz 450               state=CLOSED
t=26000ms  OK -> boAt Rockerz 450               state=CLOSED

Read the output: at t=4 s the window is [ok, ok, fail, fail, fail] = 60% → OPEN. Calls at 5 s and 8 s never touch the network. At 14 s one trial call fails → OPEN again. At 24 s the service is back: two trial successes → CLOSED.

Note: Real Resilience4j in HALF_OPEN waits for all permitted trial calls and then compares their failure rate with the threshold. The simplified rule here ("any failure re-opens") shows the same idea.