484 lines
14 KiB
Markdown
484 lines
14 KiB
Markdown
# 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.yml
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```yaml
|
|
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: true
|
|
```
|
|
|
|
### Order Service Implementation
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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.
|