
Spring Boot Resilience4j
- 1.7k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
Provides fault tolerance patterns for Spring Boot 3.x using Resilience4j. Use when implementing circuit breakers, handling service failures, adding retry logic with exponential backoff, configuring ra
About
The spring boot resilience4j skill Provides fault tolerance patterns for Spring Boot 3.x using Resilience4j. Use when implementing circuit breakers, handling service failures, adding retry logic with exponential backoff, configuring rate limiters, or protecting services from cascading failures. Generates circuit breaker, retry, rate limiter, bulkhead, time limiter, and fallback implementations. Validates resilience configurations through Actuator endpoints. Documentation covers workflows, commands, and guardrails agents should follow when users invoke this capability. Key documented areas include Implementing fault tolerance and preventing cascading failures; Adding circuit breakers, retry logic, or rate limiting to service calls; Handling transient failures with exponential backoff; Protecting services from overload and resource exhaustion. Reference commands include For Gradle, add to `build.gradle`:; Enable AOP annotation processing with `@EnableAspectJAutoProxy` (auto-configured by Spring Boot).. Use when developers or agents need structured guidance for spring boot resilience4j tasks with evidence grounded in the bundled SKILL.md rather than generic advice.
- Implementing fault tolerance and preventing cascading failures
- Adding circuit breakers, retry logic, or rate limiting to service calls
- Handling transient failures with exponential backoff
- Protecting services from overload and resource exhaustion
- Combining multiple patterns for comprehensive resilience
Spring Boot Resilience4j by the numbers
- 1,688 all-time installs (skills.sh)
- +56 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #275 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
spring-boot-resilience4j capabilities & compatibility
- Capabilities
- implementing fault tolerance and preventing casc · adding circuit breakers, retry logic, or rate li · handling transient failures with exponential bac · protecting services from overload and resource e · combining multiple patterns for comprehensive re
- Use cases
- planning
What spring-boot-resilience4j says it does
Implementing fault tolerance and preventing cascading failures
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-resilience4jAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.7k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I handle spring boot resilience4j tasks with agent guidance?
Provides fault tolerance patterns for Spring Boot 3.x using Resilience4j. Use when implementing circuit breakers, handling service failures, adding retry logic with exponential backoff, configuring ra
Who is it for?
Teams needing documented spring boot resilience4j workflows.
Skip if: Generic advice without reading bundled docs.
When should I use this skill?
Provides fault tolerance patterns for Spring Boot 3.x using Resilience4j. Use when implementing circuit breakers, handling service failures, adding retry logic with exponential backoff, configuring ra
What you get
Structured workflow from spring boot resilience4j documentation applied to the user request.
- Resilience4j bean and annotation config
- Fallback handler implementations
- Actuator-validated resilience setup
By the numbers
- Implements 6 Resilience4j fault-tolerance patterns including circuit breaker, retry, and bulkhead
Files
Spring Boot Resilience4j Patterns
Overview
Provides Resilience4j patterns (circuit breaker, retry, rate limiter, bulkhead, time limiter, fallback) for Spring Boot 3.x fault tolerance with configuration and testing workflows.
When to Use
- Implementing fault tolerance and preventing cascading failures
- Adding circuit breakers, retry logic, or rate limiting to service calls
- Handling transient failures with exponential backoff
- Protecting services from overload and resource exhaustion
- Combining multiple patterns for comprehensive resilience
Instructions
1. Setup and Dependencies
Add Resilience4j dependencies to your project. For Maven, add to pom.xml:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version> // Use latest stable version
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>For Gradle, add to build.gradle:
implementation "io.github.resilience4j:resilience4j-spring-boot3:2.2.0"
implementation "org.springframework.boot:spring-boot-starter-aop"
implementation "org.springframework.boot:spring-boot-starter-actuator"Enable AOP annotation processing with @EnableAspectJAutoProxy (auto-configured by Spring Boot).
2. Circuit Breaker Pattern
Apply @CircuitBreaker annotation to methods calling external services:
@Service
public class PaymentService {
private final RestTemplate restTemplate;
public PaymentService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
return restTemplate.postForObject("http://payment-api/process",
request, PaymentResponse.class);
}
private PaymentResponse paymentFallback(PaymentRequest request, Exception ex) {
return PaymentResponse.builder()
.status("PENDING")
.message("Service temporarily unavailable")
.build();
}
}Configure in application.yml:
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10s
instances:
paymentService:
baseConfig: defaultSee @references/configuration-reference.md for complete circuit breaker configuration options.
3. Retry Pattern
Apply @Retry annotation for transient failure recovery:
@Service
public class ProductService {
private final RestTemplate restTemplate;
public ProductService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Retry(name = "productService", fallbackMethod = "getProductFallback")
public Product getProduct(Long productId) {
return restTemplate.getForObject(
"http://product-api/products/" + productId,
Product.class);
}
private Product getProductFallback(Long productId, Exception ex) {
return Product.builder()
.id(productId)
.name("Unavailable")
.available(false)
.build();
}
}Configure retry in application.yml:
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
instances:
productService:
baseConfig: default
maxAttempts: 5See @references/configuration-reference.md for retry exception configuration.
4. Rate Limiter Pattern
Apply @RateLimiter to control request rates:
@Service
public class NotificationService {
private final EmailClient emailClient;
public NotificationService(EmailClient emailClient) {
this.emailClient = emailClient;
}
@RateLimiter(name = "notificationService",
fallbackMethod = "rateLimitFallback")
public void sendEmail(EmailRequest request) {
emailClient.send(request);
}
private void rateLimitFallback(EmailRequest request, Exception ex) {
throw new RateLimitExceededException(
"Too many requests. Please try again later.");
}
}Configure in application.yml:
resilience4j:
ratelimiter:
configs:
default:
registerHealthIndicator: true
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 500ms
instances:
notificationService:
baseConfig: default
limitForPeriod: 55. Bulkhead Pattern
Apply @Bulkhead to isolate resources. Use type = SEMAPHORE for synchronous methods:
@Service
public class ReportService {
private final ReportGenerator reportGenerator;
public ReportService(ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}
@Bulkhead(name = "reportService", type = Bulkhead.Type.SEMAPHORE)
public Report generateReport(ReportRequest request) {
return reportGenerator.generate(request);
}
}Use type = THREADPOOL for async/CompletableFuture methods:
@Service
public class AnalyticsService {
@Bulkhead(name = "analyticsService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<AnalyticsResult> runAnalytics(
AnalyticsRequest request) {
return CompletableFuture.supplyAsync(() ->
analyticsEngine.analyze(request));
}
}Configure in application.yml:
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 100ms
instances:
reportService:
baseConfig: default
maxConcurrentCalls: 5
thread-pool-bulkhead:
instances:
analyticsService:
maxThreadPoolSize: 86. Time Limiter Pattern
Apply @TimeLimiter to async methods to enforce timeout boundaries:
@Service
public class SearchService {
@TimeLimiter(name = "searchService", fallbackMethod = "searchFallback")
public CompletableFuture<SearchResults> search(SearchQuery query) {
return CompletableFuture.supplyAsync(() ->
searchEngine.executeSearch(query));
}
private CompletableFuture<SearchResults> searchFallback(
SearchQuery query, Exception ex) {
return CompletableFuture.completedFuture(
SearchResults.empty("Search timed out"));
}
}Configure in application.yml:
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 2s
cancelRunningFuture: true
instances:
searchService:
baseConfig: default
timeoutDuration: 3s7. Combining Multiple Patterns
Stack multiple patterns on a single method for comprehensive fault tolerance:
@Service
public class OrderService {
@CircuitBreaker(name = "orderService")
@Retry(name = "orderService")
@RateLimiter(name = "orderService")
@Bulkhead(name = "orderService")
public Order createOrder(OrderRequest request) {
return orderClient.createOrder(request);
}
}Execution order: Retry → CircuitBreaker → RateLimiter → Bulkhead → Method
All patterns should reference the same named configuration instance for consistency.
8. Exception Handling and Monitoring
Create a global exception handler using @RestControllerAdvice:
@RestControllerAdvice
public class ResilienceExceptionHandler {
@ExceptionHandler(CallNotPermittedException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ErrorResponse handleCircuitOpen(CallNotPermittedException ex) {
return new ErrorResponse("SERVICE_UNAVAILABLE",
"Service currently unavailable");
}
@ExceptionHandler(RequestNotPermitted.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ErrorResponse handleRateLimited(RequestNotPermitted ex) {
return new ErrorResponse("TOO_MANY_REQUESTS",
"Rate limit exceeded");
}
@ExceptionHandler(BulkheadFullException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ErrorResponse handleBulkheadFull(BulkheadFullException ex) {
return new ErrorResponse("CAPACITY_EXCEEDED",
"Service at capacity");
}
}Enable Actuator endpoints for monitoring resilience patterns in application.yml:
management:
endpoints:
web:
exposure:
include: health,metrics,circuitbreakers,retries,ratelimiters
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: trueAccess monitoring endpoints:
GET /actuator/health- Overall health including resilience patternsGET /actuator/circuitbreakers- Circuit breaker statesGET /actuator/metrics- Custom resilience metrics
Testing & Verification Workflow
1. Circuit Breaker: Call endpoint with failures → check GET /actuator/circuitbreakers shows OPEN → wait waitDurationInOpenState → verify state transitions to HALF_OPEN → CLOSED
2. Retry: Enable resilience4j.retry.metrics.enabled: true → invoke endpoint → verify retry.{instance}.successful-calls-with-retry-attempts metric increases
3. Rate Limiter: Send requests exceeding limitForPeriod → verify 429 status → check GET /actuator/ratelimiters shows LIMITED
4. Bulkhead: Load test with concurrent requests exceeding maxConcurrentCalls → verify excess requests fail immediately with BulkheadFullException
5. Time Limiter: Mock async delay beyond timeoutDuration → verify fallback triggers after timeout
See @references/testing-patterns.md for unit and integration testing strategies.
Best Practices
- Provide fallback methods: Ensure graceful degradation with meaningful responses
- Use exponential backoff: Prevent overwhelming recovering services (
exponentialBackoffMultiplier: 2) - Set appropriate thresholds:
failureRateThresholdbetween 50-70% - Use constructor injection: Never use field injection for Resilience4j dependencies
- Enable health indicators: Set
registerHealthIndicator: truefor all patterns - Retry only transient errors: Network timeouts, 5xx; skip 4xx and business exceptions
- Size bulkheads based on load: Calculate thread pool and semaphore sizes from expected concurrency
- Document fallback behavior: Make fallback logic clear and predictable
Constraints and Warnings
- Fallback methods must have the same signature plus an optional exception parameter
- Circuit breaker state is per-instance; ensure proper bean scoping in multi-tenant scenarios
- Retry operations must be idempotent (may execute multiple times)
- Do not use circuit breakers for operations that must always complete; use timeouts instead
- Rate limiters can cause thread blocking; configure appropriate wait durations
- Be cautious with
@Retryon non-idempotent operations like POST requests - Monitor memory when using thread pool bulkheads with high concurrency
Examples
Before → After: Circuit Breaker
// BEFORE: No protection
public PaymentResponse processPayment(PaymentRequest request) {
return restTemplate.postForObject("http://payment-api/process", request, PaymentResponse.class);
}
// AFTER: Circuit breaker with fallback
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
return restTemplate.postForObject("http://payment-api/process", request, PaymentResponse.class);
}
private PaymentResponse paymentFallback(PaymentRequest request, Exception ex) {
return PaymentResponse.builder().status("PENDING").message("Service temporarily unavailable").build();
}Before → After: Retry with Backoff
// BEFORE: Single attempt
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException(orderId));
}
// AFTER: Retry with exponential backoff
@Retry(name = "orderService", maxAttempts = 3, waitDuration = @WaitDuration(500L), fallbackMethod = "getOrderFallback")
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException(orderId));
}
private Order getOrderFallback(Long orderId, Exception ex) { return Order.cachedOrder(orderId); }Before → After: Rate Limiting
// BEFORE: Unbounded requests
@GetMapping("/api/data") public Data fetchData() { return dataService.process(); }
// AFTER: Rate limited
@RateLimiter(name = "dataService", fallbackMethod = "rateLimitFallback")
@GetMapping("/api/data") public Data fetchData() { return dataService.process(); }
private ResponseEntity<ErrorResponse> rateLimitFallback(Exception ex) {
return ResponseEntity.status(429).body(new ErrorResponse("TOO_MANY_REQUESTS", "Rate limit exceeded"));
}See also: Configuration Reference · Testing Patterns · Examples · Resilience4j Docs · Actuator Skill
Resilience4j Configuration Reference
Circuit Breaker Configuration
Complete Properties List
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true # Default: false
slidingWindowType: COUNT_BASED # COUNT_BASED or TIME_BASED
slidingWindowSize: 100 # Default: 100 (calls or seconds)
minimumNumberOfCalls: 10 # Default: 100
failureRateThreshold: 50 # Default: 50 (percentage)
slowCallRateThreshold: 100 # Default: 100 (percentage)
slowCallDurationThreshold: 60s # Default: 60000ms
waitDurationInOpenState: 60s # Default: 60000ms
automaticTransitionFromOpenToHalfOpenEnabled: false # Default: false
permittedNumberOfCallsInHalfOpenState: 10 # Default: 10
maxWaitDurationInHalfOpenState: 0s # Default: 0 (unlimited)
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- java.lang.IllegalArgumentException
eventConsumerBufferSize: 100 # Default: 100
instances:
myService:
baseConfig: default
failureRateThreshold: 60Circuit Breaker States
1. CLOSED: Normal operation, calls pass through 2. OPEN: Circuit is open, calls immediately fail with CallNotPermittedException 3. HALF_OPEN: Testing if service recovered, allows limited test calls 4. DISABLED: Circuit breaker disabled, all calls pass through 5. FORCED_OPEN: Manually forced to open state for emergency situations
Sliding Window Types
COUNT_BASED (Default)
- Aggregates outcome of last N calls
- Better for services with consistent traffic
slidingWindowSize= number of calls to track
TIME_BASED
- Aggregates outcome of calls in last N seconds
- Better for services with variable traffic
slidingWindowSize= time in seconds
Retry Configuration
Complete Properties List
resilience4j:
retry:
configs:
default:
maxAttempts: 3 # Default: 3
waitDuration: 500ms # Default: 500ms
enableExponentialBackoff: false # Default: false
exponentialBackoffMultiplier: 2 # Default: 2
exponentialMaxWaitDuration: 10s # Default: no limit
enableRandomizedWait: false # Default: false
randomizedWaitFactor: 0.5 # Default: 0.5
retryExceptions:
- java.io.IOException
- org.springframework.web.client.ResourceAccessException
ignoreExceptions:
- java.lang.IllegalArgumentException
failAfterMaxAttempts: false # Default: false
eventConsumerBufferSize: 100 # Default: 100
instances:
myService:
baseConfig: default
maxAttempts: 5Exponential Backoff Example
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2.0
exponentialMaxWaitDuration: 10sAttempt waits:
- Attempt 1: 500ms
- Attempt 2: 1000ms (500 × 2)
- Attempt 3: 2000ms (1000 × 2)
- Attempt 4: 4000ms (2000 × 2)
- Attempt 5: 8000ms (4000 × 2)
- Maximum: 10000ms
Rate Limiter Configuration
Complete Properties List
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 50 # Default: 50
limitRefreshPeriod: 500ns # Default: 500ns
timeoutDuration: 5s # Default: 5s
registerHealthIndicator: true # Default: false
allowHealthIndicatorToFail: true # Default: false
instances:
myService:
baseConfig: default
limitForPeriod: 10
limitRefreshPeriod: 1sCommon Rate Limit Patterns
10 requests per second
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 0s # Fail immediately if no permits100 requests per minute
limitForPeriod: 100
limitRefreshPeriod: 1m
timeoutDuration: 500ms # Wait up to 500ms for permit5 requests per second with queuing
limitForPeriod: 5
limitRefreshPeriod: 1s
timeoutDuration: 2s # Wait up to 2s for permitBulkhead Configuration
Semaphore Bulkhead
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 25 # Default: 25
maxWaitDuration: 0ms # Default: 0
eventConsumerBufferSize: 100 # Default: 100
instances:
myService:
baseConfig: default
maxConcurrentCalls: 10
maxWaitDuration: 100msThread Pool Bulkhead
resilience4j:
thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 4 # Default: Runtime.availableProcessors()
coreThreadPoolSize: 2 # Default: Runtime.availableProcessors() - 1
queueCapacity: 100 # Default: 100
keepAliveDuration: 20ms # Default: 20ms
writableStackTraceEnabled: true # Default: true
instances:
myService:
baseConfig: default
maxThreadPoolSize: 8
coreThreadPoolSize: 4
queueCapacity: 200Time Limiter Configuration
Complete Properties List
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 1s # Default: 1s
cancelRunningFuture: true # Default: true
instances:
myService:
baseConfig: default
timeoutDuration: 3sAnnotation Reference
@CircuitBreaker
@CircuitBreaker(
name = "serviceName", // Required: Instance name from config
fallbackMethod = "fallbackMethodName" // Optional: Fallback method name
)
// Fallback method signature
public String fallback(Long id, Exception ex) { }@Retry
@Retry(
name = "serviceName", // Required: Instance name from config
fallbackMethod = "fallbackMethodName" // Optional: Fallback method name
)@RateLimiter
@RateLimiter(
name = "serviceName",
fallbackMethod = "fallbackMethodName"
)@Bulkhead
@Bulkhead(
name = "serviceName",
fallbackMethod = "fallbackMethodName",
type = Bulkhead.Type.SEMAPHORE // SEMAPHORE or THREADPOOL
)@TimeLimiter
@TimeLimiter(
name = "serviceName",
fallbackMethod = "fallbackMethodName"
)
// Works only with CompletableFuture<T> or reactive types (Mono<T>, Flux<T>)Annotation Execution Order
When combining annotations on a method, execution order from outermost to innermost:
1. @Retry 2. @CircuitBreaker 3. @RateLimiter 4. @TimeLimiter 5. @Bulkhead 6. Actual method call
Exception Reference
| Pattern | Exception | HTTP Status | Meaning |
|---|---|---|---|
| Circuit Breaker | CallNotPermittedException | 503 | Circuit is OPEN or FORCED_OPEN |
| Rate Limiter | RequestNotPermitted | 429 | No permits available |
| Bulkhead | BulkheadFullException | 503 | Bulkhead at capacity |
| Time Limiter | TimeoutException | 408 | Operation exceeded timeout |
Programmatic Configuration
Circuit Breaker
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.slowCallDurationThreshold(Duration.ofSeconds(2))
.permittedNumberOfCallsInHalfOpenState(3)
.minimumNumberOfCalls(10)
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(100)
.recordExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("myService");Retry
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.intervalFunction(IntervalFunction.ofExponentialBackoff(
Duration.ofMillis(500),
2.0
))
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("myService");Actuator Endpoints
Access monitoring endpoints when management endpoints are enabled:
| Endpoint | Description |
|---|---|
GET /actuator/circuitbreakers | List all circuit breakers and states |
GET /actuator/circuitbreakerevents | List circuit breaker events |
GET /actuator/retryevents | List retry events |
GET /actuator/ratelimiters | List rate limiters |
GET /actuator/bulkheads | List bulkhead status |
GET /actuator/timelimiters | List time limiters |
GET /actuator/metrics | Custom resilience metrics |
Micrometer Metrics
Resilience4j exposes the following metrics:
Circuit Breaker Metrics
resilience4j.circuitbreaker.calls{name, kind}resilience4j.circuitbreaker.state{name, state}resilience4j.circuitbreaker.failure.rate{name}resilience4j.circuitbreaker.slow.call.rate{name}
Retry Metrics
resilience4j.retry.calls{name, kind}
Rate Limiter Metrics
resilience4j.ratelimiter.available.permissions{name}resilience4j.ratelimiter.waiting_threads{name}
Bulkhead Metrics
resilience4j.bulkhead.available.concurrent.calls{name}resilience4j.bulkhead.max.allowed.concurrent.calls{name}
Version Compatibility
| Resilience4j | Spring Boot | Java | Spring Framework |
|---|---|---|---|
| 2.2.x | 3.x | 17+ | 6.x |
| 2.1.x | 3.x | 17+ | 6.x |
| 2.0.x | 2.7.x | 8+ | 5.3.x |
| 1.7.x | 2.x | 8+ | 5.x |
References
Resilience4j Real-World Examples
E-Commerce Order Service
Complete example demonstrating all Resilience4j patterns in a microservices environment.
Project Structure
order-service/
├── src/main/java/com/ecommerce/order/
│ ├── config/
│ │ ├── ResilienceConfig.java
│ │ └── RestTemplateConfig.java
│ ├── controller/
│ │ ├── OrderController.java
│ │ └── GlobalExceptionHandler.java
│ ├── service/
│ │ ├── OrderService.java
│ │ ├── PaymentService.java
│ │ ├── InventoryService.java
│ │ └── NotificationService.java
│ ├── domain/
│ │ ├── Order.java
│ │ ├── OrderStatus.java
│ │ └── Payment.java
└── src/main/resources/
└── application.ymlConfiguration
server:
port: 8080
spring:
application:
name: order-service
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 30s
instances:
paymentService:
baseConfig: default
waitDurationInOpenState: 60s
inventoryService:
baseConfig: default
retry:
configs:
default:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
instances:
paymentService:
maxAttempts: 5
waitDuration: 1s
ratelimiter:
configs:
default:
limitForPeriod: 100
limitRefreshPeriod: 1s
instances:
emailService:
limitForPeriod: 10
limitRefreshPeriod: 1m
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 100ms
instances:
orderProcessing:
maxConcurrentCalls: 5
timelimiter:
configs:
default:
timeoutDuration: 3s
instances:
paymentService:
timeoutDuration: 5s
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: trueOrder Service Implementation
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
private final NotificationService notificationService;
@Bulkhead(name = "orderProcessing", type = Bulkhead.Type.SEMAPHORE)
@Transactional
public Order processOrder(OrderRequest request) {
log.info("Processing order for customer: {}", request.getCustomerId());
Order order = createOrder(request);
try {
// Reserve inventory
inventoryService.reserveInventory(order);
// Process payment
String paymentId = paymentService.processPayment(order).get();
order = order.toBuilder().paymentId(paymentId).build();
// Send confirmation (async, best effort)
notificationService.sendOrderConfirmation(order);
log.info("Order processed successfully: {}", order.getId());
return order;
} catch (Exception ex) {
log.error("Order processing failed", ex);
compensateFailedOrder(order);
throw new OrderProcessingException("Failed to process order", ex);
}
}
private void compensateFailedOrder(Order order) {
try {
inventoryService.releaseInventory(order);
if (order.getPaymentId() != null) {
paymentService.refundPayment(order.getPaymentId());
}
} catch (Exception ex) {
log.error("Compensation failed", ex);
}
}
}Payment Service with Multiple Patterns
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentClient paymentClient;
@CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<String> processPayment(Order order) {
return CompletableFuture.supplyAsync(() -> {
Payment payment = Payment.builder()
.orderId(order.getId())
.amount(order.getTotalAmount())
.build();
PaymentResponse response = paymentClient.processPayment(payment);
if (!response.isSuccess()) {
throw new PaymentFailedException(response.getErrorMessage());
}
return response.getPaymentId();
});
}
private CompletableFuture<String> processPaymentFallback(
Order order, Exception ex) {
log.error("Payment processing failed for order: {}", order.getId(), ex);
throw new PaymentServiceUnavailableException(
"Payment service unavailable", ex);
}
@CircuitBreaker(name = "paymentService")
@Retry(name = "paymentService")
public void refundPayment(String paymentId) {
paymentClient.refundPayment(paymentId);
}
}Exception Handler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CallNotPermittedException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ErrorResponse handleCircuitOpen(CallNotPermittedException ex) {
log.error("Circuit breaker is open", ex);
return ErrorResponse.builder()
.code("SERVICE_UNAVAILABLE")
.message("Service is temporarily unavailable")
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.build();
}
@ExceptionHandler(RequestNotPermitted.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ErrorResponse handleRateLimited(RequestNotPermitted ex) {
return ErrorResponse.builder()
.code("TOO_MANY_REQUESTS")
.message("Rate limit exceeded")
.status(HttpStatus.TOO_MANY_REQUESTS.value())
.build();
}
@ExceptionHandler(BulkheadFullException.class)
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public ErrorResponse handleBulkheadFull(BulkheadFullException ex) {
return ErrorResponse.builder()
.code("SERVICE_BUSY")
.message("Service at capacity")
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
.build();
}
@ExceptionHandler(TimeoutException.class)
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public ErrorResponse handleTimeout(TimeoutException ex) {
return ErrorResponse.builder()
.code("REQUEST_TIMEOUT")
.message("Request timed out")
.status(HttpStatus.REQUEST_TIMEOUT.value())
.build();
}
}Testing Patterns
Unit Test for Circuit Breaker
@SpringBootTest
class PaymentServiceCircuitBreakerTest {
@Autowired
private PaymentService paymentService;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@MockBean
private PaymentClient paymentClient;
private CircuitBreaker circuitBreaker;
@BeforeEach
void setup() {
circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService");
circuitBreaker.reset();
}
@Test
void shouldOpenCircuitAfterFailures() {
Order order = createTestOrder();
when(paymentClient.processPayment(any()))
.thenThrow(new RuntimeException("Service error"));
// Trigger failures to exceed threshold
for (int i = 0; i < 5; i++) {
try {
paymentService.processPayment(order).get();
} catch (Exception ignored) {}
}
assertThat(circuitBreaker.getState())
.isEqualTo(CircuitBreaker.State.OPEN);
// Next call should fail immediately
assertThatThrownBy(() -> paymentService.processPayment(order).get())
.hasRootCauseInstanceOf(PaymentServiceUnavailableException.class);
}
}Integration Test with WireMock
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void shouldRetryOnTransientFailure() {
// First two calls fail, third succeeds
stubFor(post("/payment/process")
.inScenario("Retry")
.whenScenarioStateIs(STARTED)
.willReturn(serverError())
.willSetStateTo("First Retry"));
stubFor(post("/payment/process")
.inScenario("Retry")
.whenScenarioStateIs("First Retry")
.willReturn(serverError())
.willSetStateTo("Second Retry"));
stubFor(post("/payment/process")
.inScenario("Retry")
.whenScenarioStateIs("Second Retry")
.willReturn(ok().withBody("{\"paymentId\":\"PAY-123\"}")));
Order order = orderService.processOrder(createOrderRequest());
assertThat(order.getPaymentId()).isEqualTo("PAY-123");
verify(exactly(3), postRequestedFor(urlEqualTo("/payment/process")));
}
}Advanced Scenarios
Reactive WebFlux Example
@Service
@RequiredArgsConstructor
public class ReactiveProductService {
private final WebClient webClient;
private final CircuitBreaker circuitBreaker;
private final Retry retry;
public Mono<Product> getProduct(String productId) {
return webClient.get()
.uri("/products/{id}", productId)
.retrieve()
.bodyToMono(Product.class)
.transformDeferred(CircuitBreakerOperator.of(circuitBreaker))
.transformDeferred(RetryOperator.of(retry))
.onErrorResume(throwable ->
Mono.just(Product.unavailable(productId))
);
}
}Custom Resilience Configuration
@Configuration
@Slf4j
public class ResilienceConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slowCallDurationThreshold(Duration.ofSeconds(2))
.permittedNumberOfCallsInHalfOpenState(3)
.minimumNumberOfCalls(5)
.slidingWindowSize(10)
.build();
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
// Register event consumer
registry.getEventPublisher()
.onEntryAdded(event ->
log.info("CircuitBreaker added: {}",
event.getAddedEntry().getName())
);
return registry;
}
@Bean
public RegistryEventConsumer<CircuitBreaker> circuitBreakerEventConsumer() {
return new RegistryEventConsumer<>() {
@Override
public void onEntryAddedEvent(EntryAddedEvent<CircuitBreaker> event) {
CircuitBreaker cb = event.getAddedEntry();
cb.getEventPublisher()
.onStateTransition(e ->
log.warn("CircuitBreaker {} state changed: {} -> {}",
cb.getName(),
e.getStateTransition().getFromState(),
e.getStateTransition().getToState())
)
.onError(e ->
log.error("CircuitBreaker {} error: {}",
cb.getName(),
e.getThrowable().getMessage())
);
}
@Override
public void onEntryRemovedEvent(EntryRemovedEvent<CircuitBreaker> event) {
log.info("CircuitBreaker removed: {}",
event.getRemovedEntry().getName());
}
@Override
public void onEntryReplacedEvent(EntryReplacedEvent<CircuitBreaker> event) {
log.info("CircuitBreaker replaced: {}",
event.getNewEntry().getName());
}
};
}
}Monitoring and Metrics
@RestController
@RequestMapping("/api/monitoring")
@RequiredArgsConstructor
public class ResilienceMonitoringController {
private final CircuitBreakerRegistry circuitBreakerRegistry;
@GetMapping("/circuit-breakers")
public List<CircuitBreakerStatus> getStatus() {
return circuitBreakerRegistry.getAllCircuitBreakers().stream()
.map(this::toStatus)
.collect(Collectors.toList());
}
private CircuitBreakerStatus toStatus(CircuitBreaker cb) {
CircuitBreaker.Metrics metrics = cb.getMetrics();
return CircuitBreakerStatus.builder()
.name(cb.getName())
.state(cb.getState().name())
.failureRate(metrics.getFailureRate())
.slowCallRate(metrics.getSlowCallRate())
.numberOfBufferedCalls(metrics.getNumberOfBufferedCalls())
.numberOfFailedCalls(metrics.getNumberOfFailedCalls())
.numberOfSuccessfulCalls(metrics.getNumberOfSuccessfulCalls())
.build();
}
}
@Value
@Builder
class CircuitBreakerStatus {
String name;
String state;
float failureRate;
float slowCallRate;
int numberOfBufferedCalls;
int numberOfFailedCalls;
int numberOfSuccessfulCalls;
}See testing-patterns.md for comprehensive testing strategies and configuration-reference.md for complete configuration options.
Resilience4j Testing Patterns
Circuit Breaker Testing
Testing State Transitions
@SpringBootTest
class CircuitBreakerStateTest {
@Autowired
private PaymentService paymentService;
@MockBean
private RestTemplate restTemplate;
@Test
void shouldTransitionToOpenAfterFailures() {
// Simulate repeated failures
when(restTemplate.postForObject(anyString(), any(), eq(PaymentResponse.class)))
.thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR));
// Trigger failures to exceed threshold
for (int i = 0; i < 5; i++) {
assertThatThrownBy(() -> paymentService.processPayment(new PaymentRequest()))
.isInstanceOf(HttpServerErrorException.class);
}
// Circuit should be open - fallback executes
PaymentResponse response = paymentService.processPayment(new PaymentRequest());
assertThat(response.getStatus()).isEqualTo("PENDING");
}
@Test
void shouldExecuteFallbackWhenCircuitOpen() {
when(restTemplate.postForObject(anyString(), any(), eq(PaymentResponse.class)))
.thenThrow(new RuntimeException("Service unavailable"));
// Force failures to open circuit
for (int i = 0; i < 5; i++) {
try {
paymentService.processPayment(new PaymentRequest());
} catch (Exception ignored) {}
}
// Circuit is open, fallback provides response
PaymentResponse response = paymentService.processPayment(new PaymentRequest());
assertThat(response.getStatus()).isEqualTo("PENDING");
assertThat(response.getMessage()).contains("temporarily unavailable");
}
}Testing Circuit States Directly
@SpringBootTest
class CircuitBreakerDirectStateTest {
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@Test
void shouldManuallyOpenAndCloseCircuit() {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService");
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
// Manually open circuit
circuitBreaker.transitionToOpenState();
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
// Manually close circuit
circuitBreaker.transitionToClosedState();
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
}
}Retry Testing
Testing Retry Attempts
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class RetryTest {
@Autowired
private OrderService orderService;
@Test
void shouldRetryOnTransientFailure() {
// Setup: First two calls fail, third succeeds
stubFor(post("/orders")
.inScenario("Retry Scenario")
.whenScenarioStateIs(STARTED)
.willReturn(serverError())
.willSetStateTo("First Failure"));
stubFor(post("/orders")
.inScenario("Retry Scenario")
.whenScenarioStateIs("First Failure")
.willReturn(serverError())
.willSetStateTo("Second Failure"));
stubFor(post("/orders")
.inScenario("Retry Scenario")
.whenScenarioStateIs("Second Failure")
.willReturn(ok().withBody("""
{"id":1,"status":"CREATED"}
""")));
Order order = orderService.createOrder(new OrderRequest());
assertThat(order.getId()).isEqualTo(1L);
assertThat(order.getStatus()).isEqualTo("CREATED");
// Verify exactly 3 calls were made
verify(exactly(3), postRequestedFor(urlEqualTo("/orders")));
}
@Test
void shouldThrowExceptionAfterMaxRetries() {
stubFor(post("/orders").willReturn(serverError()));
assertThatThrownBy(() -> orderService.createOrder(new OrderRequest()))
.isInstanceOf(Exception.class);
// Verify retry attempts (maxAttempts = 3)
verify(atLeast(3), postRequestedFor(urlEqualTo("/orders")));
}
}Rate Limiter Testing
Testing Rate Limit Enforcement
@SpringBootTest
class RateLimiterTest {
@Autowired
private NotificationService notificationService;
@Test
void shouldRejectRequestsExceedingRateLimit() {
// Configuration: 5 permits per second
// First 5 requests should succeed
for (int i = 0; i < 5; i++) {
notificationService.sendEmail(createEmailRequest(i));
}
// 6th request should fail immediately (no timeout)
assertThatThrownBy(() ->
notificationService.sendEmail(createEmailRequest(6))
).isInstanceOf(RequestNotPermitted.class);
}
@Test
void shouldAlowRequestsAfterWindowReset() throws InterruptedException {
// First batch of requests
for (int i = 0; i < 5; i++) {
notificationService.sendEmail(createEmailRequest(i));
}
// Wait for refresh period (1 second)
Thread.sleep(1100);
// Should succeed - window has reset
notificationService.sendEmail(createEmailRequest(5));
}
private EmailRequest createEmailRequest(int id) {
return EmailRequest.builder()
.to("user" + id + "@example.com")
.subject("Test " + id)
.build();
}
}Bulkhead Testing
Testing Semaphore Bulkhead
@SpringBootTest
class BulkheadSemaphoreTest {
@Autowired
private ReportService reportService;
@Test
void shouldLimitConcurrentCalls() {
// Configuration: maxConcurrentCalls = 5
CountDownLatch latch = new CountDownLatch(5);
List<CompletableFuture<Report>> futures = new ArrayList<>();
// Submit 5 concurrent calls
for (int i = 0; i < 5; i++) {
futures.add(CompletableFuture.supplyAsync(() -> {
latch.countDown();
return reportService.generateReport(new ReportRequest());
}));
}
// 6th call should be rejected
assertThatThrownBy(() ->
reportService.generateReport(new ReportRequest())
).isInstanceOf(BulkheadFullException.class);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}Testing Thread Pool Bulkhead
@SpringBootTest
class BulkheadThreadPoolTest {
@Autowired
private AnalyticsService analyticsService;
@Test
void shouldUseThreadPoolForAsync() {
// Configuration: threadPoolSize = 2, queueCapacity = 100
List<CompletableFuture<AnalyticsResult>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futures.add(analyticsService.runAnalytics(new AnalyticsRequest()));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (CompletableFuture<AnalyticsResult> future : futures) {
assertThat(future.join()).isNotNull();
}
}
}Time Limiter Testing
Testing Timeout Enforcement
@SpringBootTest
class TimeLimiterTest {
@Autowired
private SearchService searchService;
@Test
void shouldTimeoutExceededOperations() {
// Configuration: timeoutDuration = 1s
SearchQuery slowQuery = new SearchQuery();
slowQuery.setSimulatedDelay(Duration.ofSeconds(2));
assertThatThrownBy(() ->
searchService.search(slowQuery).get()
).hasCauseInstanceOf(TimeoutException.class);
}
@Test
void shouldReturnFallbackOnTimeout() {
SearchQuery slowQuery = new SearchQuery();
slowQuery.setSimulatedDelay(Duration.ofSeconds(2));
CompletableFuture<SearchResults> result = searchService.search(slowQuery);
SearchResults results = result.join();
assertThat(results).isNotNull();
assertThat(results.isTimedOut()).isTrue();
assertThat(results.getMessage()).contains("timed out");
}
}Fallback Method Signature Validation
Correct Fallback Signatures
@Service
public class PaymentService {
@CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
// method body
}
// CORRECT: Matches return type and parameters + Exception
private PaymentResponse paymentFallback(PaymentRequest request, Exception ex) {
// fallback logic
}
@Retry(name = "product")
public Product getProduct(String productId) {
// method body
}
// CORRECT: Can omit Exception parameter
private Product getProductFallback(String productId) {
// fallback logic
}
}Common Fallback Signature Errors
@CircuitBreaker(name = "service", fallbackMethod = "fallback")
public String processData(Long id) { }
// WRONG: Missing parameter
public String fallback(Exception ex) { }
// WRONG: Wrong return type
public void fallback(Long id, Exception ex) { }
// WRONG: Wrong parameter type
public String fallback(String id, Exception ex) { }
// CORRECT:
public String fallback(Long id, Exception ex) { }Integration Testing Configuration
Test Configuration Profile
# application-test.yml
resilience4j:
circuitbreaker:
instances:
testService:
registerHealthIndicator: false
slidingWindowSize: 5
minimumNumberOfCalls: 3
failureRateThreshold: 50
waitDurationInOpenState: 100ms
retry:
instances:
testService:
maxAttempts: 2
waitDuration: 10ms
ratelimiter:
instances:
testService:
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 10msTest Helper Methods
@TestConfiguration
public class ResilienceTestConfig {
public static void openCircuitBreaker(CircuitBreaker circuitBreaker) {
circuitBreaker.transitionToOpenState();
}
public static void closeCircuitBreaker(CircuitBreaker circuitBreaker) {
circuitBreaker.transitionToClosedState();
}
public static void simulateFailures(
CircuitBreaker circuitBreaker,
int numberOfFailures) {
for (int i = 0; i < numberOfFailures; i++) {
try {
circuitBreaker.executeSupplier(() -> {
throw new RuntimeException("Simulated failure");
});
} catch (Exception ignored) {}
}
}
public static void resetCircuitBreaker(CircuitBreaker circuitBreaker) {
circuitBreaker.transitionToClosedState();
}
}Common Testing Mistakes
Mistake 1: Not Waiting for Sliding Window
// WRONG: Circuit might not open yet
for (int i = 0; i < 3; i++) {
try { service.call(); } catch (Exception e) {}
}
assertThat(circuit.getState()).isEqualTo(CircuitBreaker.State.OPEN); // May fail!
// CORRECT: Exceed minimumNumberOfCalls before checking
for (int i = 0; i < 5; i++) { // minimumNumberOfCalls = 5
try { service.call(); } catch (Exception e) {}
}
assertThat(circuit.getState()).isEqualTo(CircuitBreaker.State.OPEN);Mistake 2: Incorrect Fallback Method Access
// WRONG: Fallback method is private, not accessible by AOP
@CircuitBreaker(name = "service", fallbackMethod = "fallback")
public String process(String data) { }
private String fallback(String data, Exception ex) { } // Private - won't work!
// CORRECT: Package-private or protected
protected String fallback(String data, Exception ex) { }Mistake 3: Not Mocking External Dependencies
// WRONG: Circuit breaker might open due to real network calls
@SpringBootTest
class ServiceTest {
@Autowired
private ServiceWithCircuitBreaker service;
// Missing @MockBean for external service
@Test
void test() {
// Real network calls - unpredictable
}
}
// CORRECT: Mock external dependencies
@SpringBootTest
class ServiceTest {
@Autowired
private ServiceWithCircuitBreaker service;
@MockBean
private ExternalService externalService;
@Test
void test() {
when(externalService.call()).thenThrow(new RuntimeException());
// Predictable failure
}
}Performance Considerations
Memory Usage in Tests
- COUNT_BASED sliding window: Stores last N call outcomes in memory
- TIME_BASED sliding window: May require more memory for high-throughput services
- Use smaller
slidingWindowSizein tests to reduce memory footprint
Timeout Configuration for Tests
resilience4j:
timelimiter:
instances:
testService:
timeoutDuration: 2s # Longer timeout for slower CI/CD environments
circuitbreaker:
instances:
testService:
waitDurationInOpenState: 100ms # Shorter for faster test executionAvoiding Test Flakiness
- Set deterministic timeouts based on CI/CD environment
- Use
@ActiveProfiles("test")for test-specific configurations - Reset circuit breaker state between tests when needed
- Mock external services consistently
Related skills
Forks & variants (1)
Spring Boot Resilience4j has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
How it compares
Choose this over generic retry snippets when you need full Resilience4j pattern coverage with Spring Boot 3.x Actuator validation.
FAQ
What does spring boot resilience4j do?
Provides fault tolerance patterns for Spring Boot 3.x using Resilience4j. Use when implementing circuit breakers, handling service failures, adding retry logic with exponential backoff, configuring ra
When should I invoke spring boot resilience4j?
Provides fault tolerance patterns for Spring Boot 3.x using Resilience4j. Use when implementing circuit breakers, handling service failures, adding retry logic with exponential backoff, configuring ra
What are key capabilities?
Implementing fault tolerance and preventing cascading failures
Is Spring Boot Resilience4j safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.