Programmatic Usage
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Annotations are the shortest way to protect a method, but they only see a method call: they cannot see the HTTP status
line of the response, they cannot differ per request, and they do not work on code that is not a Spring bean. Using the
registries and decorators directly puts the resilience where the failure actually happens — around the HTTP call itself — and works on every client. This page shows how, for the four ways a Spring Boot application calls another service:
RestClient, WebClient, the JDK HttpClient and Spring HTTP interface clients (@HttpExchange). It was written against
Resilience4j 2.4.0, Spring Boot 4.1.x, Spring Framework 7.0.x and Java 21, and every example on it was compiled and run
against WireMock stubs on 4 October 2026. The Java API is the same on Spring Boot 3.5.x (starter
resilience4j-spring-boot3, Java package io.github.resilience4j.springboot3); the two Boot-specific differences are
noted where they occur.
The model used here (registries, configurations, decorateXxx, Decorators) is explained in
Registries, Configuration & Decorators
and is not repeated. How the HTTP clients themselves are built and used is covered by
REST APIs and Reactive
Programming; this page only adds resilience to them. The setup (starter, spring-boot-starter-aspectj for annotations)
is in Getting Started; programmatic use does not need the aspect
starter at all.
In the running Bookshelf scenario, bookshelf-service calls pricing-service with RestClient, reviews-service with
WebClient, the third-party ISBN-metadata API with the JDK HttpClient, and recommendations-service through an
@HttpExchange interface. Each downstream gets its own instances, named after it.
Injecting the registries
With the starter on the classpath, the registries are ordinary Spring beans, created from application.yml at startup
(see Registries in
Spring Boot). Inject the ones you need and ask them for an instance by name: the name is the key under
resilience4j.<module>.instances, and an instance listed in the YAML already exists when your bean is created. This is
the configuration used by every example on this page:
resilience4j:
circuitbreaker:
configs:
http:
slidingWindowType: COUNT_BASED
slidingWindowSize: 4
minimumNumberOfCalls: 4
failureRateThreshold: 50
waitDurationInOpenState: 30s
recordResultPredicate: com.example.bookshelf.http.HttpFailureResult
instances:
pricing:
baseConfig: http
recommendations:
baseConfig: http
reviews:
baseConfig: http
bulkhead:
instances:
pricing:
maxConcurrentCalls: 10
recommendations:
maxConcurrentCalls: 4
maxWaitDuration: 0
retry:
instances:
pricing:
maxAttempts: 3
waitDuration: 100ms
retryExceptions:
- java.io.IOException
recommendations:
maxAttempts: 1
Source: Resilience4j: Spring Boot 2 & 3 (configs, instances,
baseConfig) and the
v2.4.0
CommonCircuitBreakerConfigurationProperties source (recordResultPredicate).
In the run, the CircuitBreakerRegistry held exactly pricing, recommendations and reviews after startup, the pricing
breaker had the window of 4 from the shared http configuration, and the retry for pricing had 3 attempts. The
recordResultPredicate key is explained under Counting 5xx and 429 as failures.
@Service
class PricingService {
private final PricingClient client;
private final CircuitBreaker breaker;
private final Retry retry;
PricingService(PricingClient client, CircuitBreakerRegistry breakers, RetryRegistry retries) {
this.client = client;
this.breaker = breakers.circuitBreaker("pricing"); // created from the YAML at startup
this.retry = retries.retry("pricing");
}
Money priceOf(String isbn) {
// run it now: Retry outside, CircuitBreaker inside
return Decorators.ofSupplier(() -> client.fetch(isbn))
.withCircuitBreaker(breaker)
.withRetry(retry)
.withFallback(List.of(CallNotPermittedException.class), t -> Money.unknown())
.get();
}
Supplier<Money> priceOfLater(String isbn) {
// build it now, run it later (decorateSupplier returns a Supplier)
return CircuitBreaker.decorateSupplier(breaker, () -> client.fetch(isbn));
}
CompletionStage<Money> priceAsync(String isbn) {
// a call that already returns a CompletionStage
return breaker.executeCompletionStage(() -> client.fetchAsync(isbn));
}
}
Source: Resilience4j: CircuitBreaker examples (executeSupplier,
decorateSupplier, executeCompletionStage) and the
v2.4.0
CircuitBreaker source.
Look the instance up once, in the constructor, and keep it in a field. Calling breakers.circuitBreaker("pricing") on
every request works (the registry returns the same object) but spreads names through the code, and a misspelled name
silently creates a new instance from the default configuration instead of failing. The same applies to the other registries; on a name that
the YAML does not declare, retries.retry("pricng") quietly gives you a three-attempt retry from the default.
Where to put the decoration
An HTTP client has three places where a decorator can go, and the choice decides what the breaker can see.
| Layer | What the decorator sees | Typical use |
|---|---|---|
Around your method ( |
The method’s return value or its exception. With |
Business-level calls, mixed sources (a cache, then HTTP), code that is not an HTTP client. |
Inside the client (interceptor, filter, wrapper) |
The raw response, before Spring maps a status to an exception. You can classify by status code and read headers. |
Every call made through that client, including calls made by code that does not know about resilience. |
Around the transport (a custom |
Connections and bytes. |
Rarely worth it; use the client’s own timeouts and connection pool instead. |
This page uses the second layer for HTTP, one decorated client per downstream service, because it is the only layer where a
503 is still a response and not an exception. The next figure shows the chain for the Bookshelf pricing call.
Counting 5xx and 429 as failures
By default a circuit breaker records a call as a failure when it throws. An HTTP client that returns a response object
for a 503 or a 429 did not throw, so the breaker sees a success: the verified run sent six 503 answers through a
RestClient interceptor with a default breaker and the breaker stayed CLOSED with four recorded successes and no
failures. recordResultPredicate closes that gap: it is evaluated on the value the decorated call returns, and a match
records a failure (the event carries a ResultRecordedAsFailureException) while the value is still returned unchanged.
The circuit-breaker page explains the rule in
What counts as a failure; here it is
applied to HTTP.
The predicate is a Predicate<Object>, so one class can serve every client in the application: it matches the response
type each of them produces.
public class HttpFailureResult implements Predicate<Object> {
@Override
public boolean test(Object result) {
int status = switch (result) {
case ClientHttpResponse response -> statusOf(response); // RestClient interceptor
case ClientResponse response -> response.statusCode().value(); // WebClient filter
case HttpResponse<?> response -> response.statusCode(); // JDK HttpClient
case ResponseEntity<?> entity -> entity.getStatusCode().value();
default -> -1;
};
return status >= 500 || status == 429;
}
private static int statusOf(ClientHttpResponse response) {
try {
return response.getStatusCode().value();
} catch (IOException e) {
return 599; // an unreadable status line is a failure
}
}
}
Source: Resilience4j: CircuitBreaker (recordResultPredicate) and the
v2.4.0
CircuitBreakerConfig source (recordResult).
Two ways to attach it. In YAML, name the class under recordResultPredicate, as in the configuration above; the class
is created by reflection through its public no-argument constructor, so it is a plain object, not a Spring bean. In Java,
pass an instance to the builder:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slidingWindowSize(4).minimumNumberOfCalls(4).failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.recordResult(new HttpFailureResult()) // 5xx and 429 on the returned response
.build();
CircuitBreaker breaker = CircuitBreakerRegistry.of(config).circuitBreaker("pricing");
Source: Resilience4j: CircuitBreaker (recordResult on the config
builder).
The mapping applies to the breaker only. A 404 or a 400 is the caller’s problem, not the dependency’s, so it is a
success for the breaker; in the verified run the sequence 503, 429, 404, 200 gave two failures out of four and
opened a breaker with a 50 per cent threshold. A 429 is deliberately a failure here: the downstream is telling you to
slow down, and opening the breaker stops you from hammering it. If the upstream sends Retry-After, the limiter pages
(Rate Limiter) and the retry page
(Retry) show how to honour it.
RestClient: a ClientHttpRequestInterceptor
RestClient runs every request through its ClientHttpRequestInterceptor chain. The interceptor receives the request, the
body and an execution; calling execution.execute(request, body) performs the call and returns the raw
ClientHttpResponse, before the retrieve() status handlers turn a 4xx or a 5xx into an exception. That makes the
interceptor the right place for a breaker, a bulkhead and a retry: they decorate execution.execute with
Decorators.ofCheckedSupplier, because the call throws IOException.
public class ResilienceInterceptor implements ClientHttpRequestInterceptor {
private final CircuitBreaker breaker;
private final Bulkhead bulkhead;
private final Retry retry;
public ResilienceInterceptor(CircuitBreaker breaker, Bulkhead bulkhead, Retry retry) {
this.breaker = breaker;
this.bulkhead = bulkhead;
this.retry = retry;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
CheckedSupplier<ClientHttpResponse> call = Decorators
.ofCheckedSupplier(() -> execution.execute(request, body))
.withCircuitBreaker(breaker) // innermost: sees every attempt
.withBulkhead(bulkhead)
.withRetry(retry) // outermost: retries only what the breaker lets through
.decorate();
try {
return call.get();
} catch (IOException | RuntimeException e) {
throw e; // CallNotPermittedException, BulkheadFullException, I/O errors
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
}
Source: Spring Framework: REST Clients
(ClientHttpRequestInterceptor) and Resilience4j: CircuitBreaker examples
(the Decorators builder).
Building a client with it, and what the run showed:
RestClient pricing = restClientBuilder
.baseUrl("http://localhost:8081")
.requestInterceptor(new ResilienceInterceptor(
breakers.circuitBreaker("pricing"), bulkheads.bulkhead("pricing"), retries.retry("pricing")))
.build();
Money price = pricing.get().uri("/price/{isbn}", isbn).retrieve().body(Money.class);
Source: Spring Framework: REST Clients.
On Spring Boot 4 the injected RestClient.Builder bean comes from spring-boot-starter-restclient; with only
spring-boot-starter-web there is none (on Boot 3.5 the web starter is enough).
-
With the
HttpFailureResultpredicate, the calls/price/503,/price/429,/price/404and/price/okthrewHttpServerErrorException,HttpClientErrorException.TooManyRequestsandHttpClientErrorException.NotFound, and returned the body, asRestClientalways does; the breaker recorded two failures and two successes and opened. -
The next call threw
CallNotPermittedExceptionstraight out ofretrieve().body(…), not wrapped in a Spring exception, and WireMock received no request: an open breaker really does protect the downstream. -
A
CONNECTION_RESET_BY_PEERfault on the first attempt followed by a good answer was retried by the interceptor (retryExceptions: IOException), so the caller got the body, and the breaker recorded one failure and one success.
Three details are specific to interceptors. The retry replays execution.execute(request, body), which is safe because
the body is a byte[]; do not retry non-idempotent requests (see
Retry). A retry that is triggered by a result rather than an exception
must close the discarded ClientHttpResponse itself, or the connection leaks, which is why the interceptor above retries
on IOException only. And the interceptor blocks the calling thread while Retry sleeps, which is cheap on virtual
threads and costly on a small platform-thread pool; see
Concurrency Alternatives.
WebClient: an ExchangeFilterFunction
WebClient has no interceptor; its extension point is an ExchangeFilterFunction, which wraps next.exchange(request),
a Mono<ClientResponse>. The resilience4j-reactor operators (CircuitBreakerOperator, BulkheadOperator, and the
others, all applied with transformDeferred) turn the decorators into operators on that Mono; the operator
catalogue and the order of operators are in
Reactive, Kotlin & RxJava. The operator
honours recordResultPredicate, so the same HttpFailureResult works on the ClientResponse.
ExchangeFilterFunction resilience = (request, next) -> next.exchange(request)
.transformDeferred(CircuitBreakerOperator.of(breakers.circuitBreaker("reviews")))
.transformDeferred(BulkheadOperator.of(bulkheads.bulkhead("reviews")));
WebClient reviews = webClientBuilder
.baseUrl("http://localhost:8082")
.filter(resilience)
.build();
Flux<String> list = reviews.get().uri("/reviews/{isbn}", isbn).retrieve().bodyToFlux(String.class);
Source: Spring Framework: WebClient
filters and Resilience4j examples (Reactor operators). On Spring Boot 4 the
WebClient.Builder bean comes from spring-boot-starter-webclient.
In the run, /reviews/503 and /reviews/429 reached the caller as WebClientResponseException.ServiceUnavailable and
TooManyRequests, two 200 answers followed, and the breaker was OPEN with two recorded failures; the next call failed
with CallNotPermittedException and the bulkhead showed all five permits free again, because the permit taken
by the outer bulkhead operator is released when the inner breaker rejects the call. With transformDeferred, the operator applied last
is the outermost, so the order above makes the bulkhead outer and the breaker inner; swap the two lines to reject before
queueing for a permit. Because the filter is applied per exchange, a new subscription is protected on each
subscribe; there is nothing to reset.
The JDK HttpClient: wrap the call
java.net.http.HttpClient has no interceptor or filter. Decorate the call yourself, in a small wrapper class that
is the only way the rest of the application reaches the downstream. HttpClient.send throws checked exceptions, so use the
Checked* decorators (or executeCheckedSupplier); for sendAsync, which returns a CompletableFuture, use
executeCompletionStage. In both cases the decorated value is an HttpResponse, and HttpFailureResult already
understands it.
class IsbnApi {
private final HttpClient http = HttpClient.newHttpClient();
private final CircuitBreaker breaker;
private final CheckedFunction<HttpRequest, HttpResponse<String>> send;
IsbnApi(CircuitBreakerRegistry breakers) {
this.breaker = breakers.circuitBreaker("isbn");
this.send = CircuitBreaker.decorateCheckedFunction(breaker,
request -> http.send(request, HttpResponse.BodyHandlers.ofString()));
}
HttpResponse<String> lookup(String isbn) throws Throwable {
return send.apply(HttpRequest.newBuilder(URI.create("https://isbn.example.invalid/v1/" + isbn)).build());
}
CompletionStage<HttpResponse<String>> lookupAsync(String isbn) {
HttpRequest request = HttpRequest.newBuilder(URI.create("https://isbn.example.invalid/v1/" + isbn)).build();
return breaker.executeCompletionStage(
() -> http.sendAsync(request, HttpResponse.BodyHandlers.ofString()));
}
}
Source: Java 21
java.net.http.HttpClient and Resilience4j examples (checked decorators,
executeCompletionStage).
Unlike RestClient, the JDK client never turns a status into an exception, so every 503 and 429 is a response. In
the run, with isbn configured with the HTTP predicate, the sequence 503, 429, 200, 200 returned the four
responses to the caller (status codes 503, 429, 200, 200) and left the breaker OPEN with two failures; four 503 answers
through sendAsync also opened the breaker, because executeCompletionStage evaluates the predicate when the future
completes. Without the predicate neither would have opened it. Use ofString or another handler per call; the
predicate looks at the status only and never touches the body.
Spring HTTP interface clients (@HttpExchange)
A @HttpExchange interface is a declarative client: Spring generates an implementation (an HttpServiceProxyFactory
proxy) that delegates to a RestClient (or a WebClient). Because the proxy sits on top of the client, you have two places
to add resilience, and you choose by the layer table above.
Resilience in the underlying client. Build the proxy on a RestClient that has the interceptor, and every method of the
interface is covered, with the response available to the predicate.
@HttpExchange("/recommendations")
public interface RecommendationsApi {
@GetExchange("/{isbn}")
List<String> forIsbn(@PathVariable String isbn);
}
RestClient client = RestClient.builder().baseUrl(baseUrl)
.requestInterceptor(new ResilienceInterceptor(
breakers.circuitBreaker("recommendations"), bulkheads.bulkhead("recommendations"),
retries.retry("recommendations")))
.build();
RecommendationsApi api = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build()
.createClient(RecommendationsApi.class);
Source: Spring Framework:
HTTP Interface (HttpServiceProxyFactory, RestClientAdapter).
In the run, forIsbn("ok") returned the list, three 503 answers each threw HttpServerErrorException out of the proxy
and opened the breaker (three failures out of four calls), and the next call threw CallNotPermittedException from the
proxy.
Resilience around the proxy method. If the client is built elsewhere (a library, a group configured by Spring Boot) and you cannot reach its interceptors, decorate the call instead. The breaker now sees exceptions, not responses, so the 5xx and 429 rule has to be an exception predicate:
CircuitBreakerConfig byException = CircuitBreakerConfig.custom()
.slidingWindowSize(2).minimumNumberOfCalls(2)
.recordException(t -> t instanceof ResourceAccessException
|| t instanceof HttpServerErrorException
|| t instanceof HttpClientErrorException.TooManyRequests)
.build();
List<String> recommendations = breaker.executeSupplier(() -> api.forIsbn(isbn));
Source: Resilience4j: CircuitBreaker (recordExceptions,
recordFailurePredicate). The equivalent YAML key is recordFailurePredicate.
Two failing calls opened the breaker in the run. Do not decorate both the client and the method for the same instance: each layer would record the same failed response, and the window would fill twice as fast.
One interceptor or filter per downstream
The Bookshelf services have different latencies, quotas and failure modes, so they must not share a breaker: if
recommendations-service is down, pricing-service has to keep working. The rule that follows is one decorated client
per downstream service, one instance of each pattern named after it. In the code above that is the argument to
breakers.circuitBreaker("pricing"); shared settings live in a configs entry (http in the YAML) and each downstream
only overrides what differs (a smaller bulkhead for the optional recommendations). Instance names then appear in the
Actuator endpoints and in the metrics tags, so a dashboard can show pricing and recommendations separately.
When the clients are @HttpExchange interfaces grouped by Spring, Spring Boot gives you the per-downstream hook for free:
a bean of type RestClientHttpServiceGroupConfigurer customizes the RestClient.Builder of each group, and the group
name can be the Resilience4j instance name. The group’s base URL is a Boot property.
@Configuration
@ImportHttpServices(group = "recommendations", types = RecommendationsApi.class)
class HttpServiceClients {
@Bean
RestClientHttpServiceGroupConfigurer resilientGroups(CircuitBreakerRegistry breakers,
BulkheadRegistry bulkheads, RetryRegistry retries) {
return groups -> groups.forEachClient((group, builder) -> builder.requestInterceptor(
new ResilienceInterceptor(breakers.circuitBreaker(group.name()), bulkheads.bulkhead(group.name()),
retries.retry(group.name()))));
}
}
Source: Spring Framework:
HTTP Interface (@ImportHttpServices, HttpServiceGroupConfigurer).
spring:
http:
serviceclient:
recommendations:
base-url: http://localhost:8083
Source: Spring Boot: Calling REST Services (HTTP service
clients and groups; the spring.http.serviceclient.* properties).
In the run the injected RecommendationsApi bean was the proxy over the group’s client: four 503 answers opened the
recommendations breaker and the fifth call threw CallNotPermittedException. This is the Boot 4 and Spring Framework 7
route (@ImportHttpServices, spring.http.serviceclient.*); on Spring Boot 3.5 build the proxy by hand with
HttpServiceProxyFactory as in the previous example. For a group named differently from the instance, map one to the
other explicitly; and note that retries.retry(group.name()) for a name that is not in the YAML silently creates a retry with the
default three attempts, so declare every group in the configuration.
Programmatic or annotations?
| Question | Prefer annotations when | Prefer programmatic use when |
|---|---|---|
What does the code protect? |
A Spring bean’s method with a simple return type. |
An HTTP client, a library, code outside a Spring bean, or a call made from a lambda. |
What must the pattern see? |
The method’s exception or return value. |
The HTTP response (status, headers) so that 5xx and 429 can be recorded, or a result predicate. |
Is the instance fixed? |
The instance name is a constant known at compile time. |
The instance depends on the request (a tenant, a route) or on configuration read at runtime. |
Order of the patterns |
The default order (Retry, CircuitBreaker, RateLimiter, TimeLimiter, Bulkhead) is what you want. |
You need a different order on one call, or a pattern that has no annotation (cache, hedge) in the same chain. |
Proxy limits |
The method is public, non-final and called from another bean (no self-invocation). |
It is not: the call is a private method, a self-invocation or an object that Spring does not proxy; no proxy is involved at all. |
Testing |
A Spring context with the aspect starter is needed to exercise the annotation. |
Plain unit tests: create the instance with |
The two styles combine: annotate the service methods and put the HTTP-level rules in the interceptor, which is what most
applications end up doing. The details of annotations, including the aspect order that applies to them, are in
Annotations & Aspect Order. Programmatic use does
not depend on the aspect starter, so a library that must not force spring-boot-starter-aspectj on its users should
use it. Testing the interceptors with WireMock, as the examples here were run, is the subject of
Testing.
Pitfalls
| Pitfall | What happens and what to do |
|---|---|
Decorating the client but not telling the breaker about 5xx |
|
The same failure recorded twice |
Decorating both the interceptor and the |
A mistyped instance name |
|
The interceptor wraps the exception |
It does not: |
Retrying on a result without closing the response |
A discarded |
Predicate class with injected dependencies |
|
Reading the body in a result predicate |
The predicate runs before the caller reads the body; consuming it there breaks the response. Look at the status and headers only. |
One breaker for several downstreams |
One slow dependency opens the circuit for all of them. One instance per downstream, a shared |
Boot 4 starters |
|
What changed recently
-
Spring Framework 7 and Spring Boot 4 added
@ImportHttpServices,RestClientHttpServiceGroupConfigurerand thespring.http.serviceclient.*properties, so a group of HTTP interface clients can be configured, and wrapped with an interceptor, in one place. -
Spring Boot 4 split the HTTP clients into their own starters (
spring-boot-starter-restclient,spring-boot-starter-webclient); on Boot 3.5 the web and webflux starters provide the builders. -
Resilience4j 2.1.0 made
recordResultPredicateconfigurable from Spring Boot properties. -
Resilience4j 2.4.0 adds
resilience4j-spring-boot4; the registries,Decoratorsand the operators used on this page are unchanged from 2.3.x. The release notes are only on the GitHub release page.
References
-
Spring Framework: REST Clients (
RestClient, interceptors, HTTP Interface) -
RFC 6585: Additional HTTP Status Codes (429) and RFC 9110: HTTP Semantics