Build a circuit breaker with Resilience4j and Spring Boot

Most Spring Boot applications depend on services they do not control. A checkout can call a payment gateway, a fraud engine and an address service, so a slow provider can tie up request threads while customers wait. A circuit breaker gives those calls a controlled failure path and stops repeated requests to an unhealthy dependency.

Resilience4j brings the circuit breaker pattern to Spring Boot through annotations and a programmatic API. Its state machine moves from CLOSED to OPEN after enough failures, then permits a few trial calls in HALF_OPEN. Successful probes close the circuit; failed probes reopen it.

The implementation below uses Spring Boot 3, a REST payment call, YAML configuration and deliberate fallback behaviour. It also covers observability, testing and the operational details that matter for Australian deployments.

Scope the failure boundary

Apply the circuit breaker around a logical downstream operation, such as charging a card, rather than around an entire controller. A shared instance for every external call would open because one provider is unhealthy, even when another remains reliable.

Timeouts and retries are separate controls. A retry can increase load on a struggling provider, while retrying a non-idempotent charge can create duplicate payments. Use retries only where the operation is idempotent or the provider supports an idempotency key, and give each dependency its own breaker.

Configure the breaker for a payment provider

Add spring-boot-starter-aop and the Resilience4j Spring Boot module. This example targets Spring Boot 3; older projects should use the matching Resilience4j module and a compatible release.

Configure a named instance in application.yml:

resilience4j:
  circuitbreaker:
    instances:
      paymentGateway:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s
        slowCallRateThreshold: 80
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 3
        automaticTransitionFromOpenToHalfOpenEnabled: true
        registerHealthIndicator: true
        recordExceptions:
          - org.springframework.web.client.ResourceAccessException
          - org.springframework.web.client.HttpServerErrorException
          - java.util.concurrent.TimeoutException

The window counts recent calls, while the thresholds decide when the circuit opens. The open-state wait controls how long the service waits before allowing trial requests. Restricting recorded exceptions prevents ordinary client errors or application bugs from opening the circuit.

Handle failures with a fallback

Create a RestClient bean with the provider’s base URL, then inject it into the service. Annotate the operation and provide a fallback with matching arguments plus a Throwable:

@Service
public class PaymentService {

    private final RestClient paymentClient;

    public PaymentService(RestClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @CircuitBreaker(
        name = "paymentGateway",
        fallbackMethod = "chargePaymentFallback"
    )
    public PaymentResponse chargePayment(PaymentRequest request) {
        return paymentClient.post()
                .uri("/charges")
                .body(request)
                .retrieve()
                .body(PaymentResponse.class);
    }

    public PaymentResponse chargePaymentFallback(
            PaymentRequest request, Throwable cause) {
        return PaymentResponse.pending();
    }
}

The fallback should express business truth. Returning a pending status makes sense if the request can be safely queued; it must not masquerade as a confirmed payment. Because Spring applies the annotation through a proxy, callers must enter through the service bean rather than invoke the method internally on the same object.

Make dependency failures visible

Add Spring Boot Actuator and expose the circuit-breaker endpoint alongside health and metrics:

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,circuitbreakers

These endpoints expose call volume, state changes and latency information for dashboards and diagnostics. Log transitions with a dedicated logger, and include a correlation identifier in payment records so engineers can connect a failed charge with the exact provider request.

In an Australian support chat, “no worries, we’re on it” can keep a customer calm. The incident record still needs the provider name, circuit state and correlation identifier, especially when a brief regional mobile-network fault resembles a complete payment outage.

Test the failure transitions

Use MockRestServiceServer or WireMock to simulate provider behaviour. Arrange repeated server errors to move the circuit from CLOSED to OPEN, confirm later calls take the fallback, then wait beyond the configured open-state duration to exercise HALF_OPEN. A short test-specific wait duration keeps the suite quick.

Include slow responses, provider validation errors and successful recovery. A 4xx response should normally reach the caller without recording a dependency failure, while a timeout should count only when its exception type appears in the configuration.

Practical recommendations for resilient deployments

Compare the open-state duration with the provider’s actual recovery time before rollout. Australian traffic can cross Sydney, Melbourne and Perth, and regional fixed-line and mobile connections vary, so timeout budgets should tolerate normal network latency without hiding a genuine outage. Local payment operations also involve Australian-dollar amounts, GST-inclusive consumer totals and support handoffs across time zones affected by daylight-saving changes.

For an independent architecture or production-readiness review, JavaWhizz provides IT consultancy guidance alongside hands-on implementation support.