# 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 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 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 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 circuitBreakerEventConsumer() { return new RegistryEventConsumer<>() { @Override public void onEntryAddedEvent(EntryAddedEvent 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 event) { log.info("CircuitBreaker removed: {}", event.getRemovedEntry().getName()); } @Override public void onEntryReplacedEvent(EntryReplacedEvent 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 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.