Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot/spring-boot-resilience4j/references/examples.md
2025-11-29 18:28:30 +08:00

14 KiB

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

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

@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.