---
name: http-resilience
description: "Make outbound HTTP calls resilient in an existing Spring Boot 4 Maven project — circuit breakers, retries, and timeouts with RestClient and Resilience4j, plus tests that prove failures degrade gracefully. Use when asked to add retries or a circuit breaker, call external APIs reliably, or protect against a flaky downstream. Not for inbound rate limiting — use redis-setup."
---
# HTTP Resilience Skill
Adds outbound-call resilience to an existing Spring Boot 4 project: RestClient with explicit
timeouts and Resilience4j fault tolerance around every downstream call.
`SKILL_DIR` = directory containing this SKILL.md file.
---
## Step 0 — Gather inputs
| Field | Required | Notes |
|-------|----------|-------|
| `service` | Yes | the downstream being called (e.g. `payments`, base URL) |
| `endpoints` | Yes | which calls need protection |
| `failureMode` | No | fallback value vs fail-fast (default: fail-fast with 502) |
Next: read the project.
---
## Step 1 — Read the project
```bash
cat pom.xml
grep -rn "RestClient\|RestTemplate\|WebClient\|@FeignClient" src/main/java/ | head -10
ls src/main/java/**/*client* 2>/dev/null
```
Confirm: Maven project on Boot 4. Find existing HTTP call sites — retrofit them rather than adding
a parallel client.
Next: add Resilience4j.
---
## Step 2 — Dependencies
Add to `pom.xml`:
```xml
io.github.resilience4j
resilience4j-spring-boot4
2.4.0
org.springframework.boot
spring-boot-starter-aspectj
```
The starter needs AspectJ on the classpath — Boot no longer pulls it transitively. (Boot 4 renamed
`spring-boot-starter-aop` to `spring-boot-starter-aspectj`; the old name stopped at 4.0.0-M2.)
Before writing, verify `2.4.0` is still the latest:
```bash
curl -s "https://repo1.maven.org/maven2/io/github/resilience4j/resilience4j-spring-boot4/maven-metadata.xml" \
| python3 -c "import sys,re; print(re.findall(r'(.*?)', sys.stdin.read())[-1])"
```
Next: the client.
---
## Step 3 — RestClient with explicit timeouts
Create or refactor the client (example: `payment/PaymentClient.java`):
```java
package com.example.payment;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
@Configuration
class PaymentClientConfig {
@Bean
RestClient paymentRestClient(RestClient.Builder builder) {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(2));
factory.setReadTimeout(Duration.ofSeconds(5));
return builder.baseUrl("http://localhost:9090").requestFactory(factory).build();
}
}
```
Rules:
- Every client gets connect AND read timeouts. No timeout is the default outage amplifier.
- One `RestClient` bean per downstream, named after it.
- Base URL comes from config (Step 4), not a literal — shown inline here for shape only.
Next: wrap the calls.
---
## Step 4 — Circuit breaker, retry, config
Annotate the calling methods:
```java
@CircuitBreaker(name = "payments", fallbackMethod = "fallbackCharge")
@Retry(name = "payments")
public ChargeResult charge(ChargeRequest request) {
return paymentRestClient.post().uri("/charges").body(request).retrieve()
.body(ChargeResult.class);
}
```
Externalize in `application.yml`:
```yaml
resilience4j:
circuitbreaker:
instances:
payments:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
retry:
instances:
payments:
max-attempts: 3
wait-duration: 500ms
enable-exponential-backoff: true
```
Rules:
- Retry only idempotent calls (GET, or POST with an idempotency key). Say so per call.
- Fallbacks return a domain value or throw a 502-mapped exception — never swallow silently.
- `@TimeLimiter` needs a `CompletableFuture` return type; skip it unless the user asks for async.
Next: prove it degrades.
---
## Step 5 — Test the failure path
Integration test with a dead downstream (no mocking of Resilience4j itself):
```java
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
@SpringBootTest
class PaymentClientResilienceIT {
@Autowired PaymentClient client;
@Test
void opensCircuitAfterFailures() {
// downstream unreachable: connection refused
for (int i = 0; i < 10; i++) {
assertThatThrownBy(() -> client.charge(new ChargeRequest(100)))
.isInstanceOf(Exception.class);
}
assertThatThrownBy(() -> client.charge(new ChargeRequest(100)))
.isInstanceOf(CallNotPermittedException.class); // circuit open, call not attempted
}
}
```
The assertion that matters: once the circuit is open, calls fail fast with
`CallNotPermittedException` instead of waiting on timeouts.
Next: report.
---
## Step 6 — Report
Report:
- Dependencies added (with verified versions)
- Clients created/refactored and their timeouts
- Resilience4j instances configured and their policies
- Which calls are retried and why they are idempotent
- Test result: circuit opens and fails fast
- Next step: expose breaker state on `/actuator/health` by adding `management.health.circuitbreakers.enabled: true`