Fallback Methods in Resilience4j in Java

Overview

Fallback methods in Resilience4j provide alternative behavior when a primary operation fails. They are essential for building resilient systems that can gracefully handle failures and maintain functionality.

Core Concepts

  • Fallback: Alternative execution path when main operation fails
  • Exception Handling: Specific fallbacks for different exception types
  • Recovery Strategies: Various approaches to handle failures
  • Composition: Combining fallbacks with other resilience patterns

Dependencies

Maven Dependencies

<!-- Resilience4j Core --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.1.0</version> </dependency> <!-- Circuit Breaker --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> <version>2.1.0</version> </dependency> <!-- Retry --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-retry</artifactId> <version>2.1.0</version> </dependency> <!-- Spring Boot AOP (for annotations) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- For reactive support --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-reactor</artifactId> <version>2.1.0</version> </dependency>

Basic Fallback Configuration

1. Configuration Properties

application.yml:

resilience4j: circuitbreaker: instances: productService: register-health-indicator: true failure-rate-threshold: 50 slow-call-rate-threshold: 50 slow-call-duration-threshold: "2s" permitted-number-of-calls-in-half-open-state: 10 sliding-window-type: COUNT_BASED sliding-window-size: 100 wait-duration-in-open-state: "10s" automatic-transition-from-open-to-half-open-enabled: true retry: instances: productService: max-attempts: 3 wait-duration: "500ms" enable-exponential-backoff: true exponential-backoff-multiplier: 2 timelimiter: instances: productService: timeout-duration: "5s" # Custom fallback configuration app: resilience: fallback: default-timeout: 3000 cache-ttl: 600000 max-retry-attempts: 3

2. Basic Fallback Examples

import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import org.springframework.stereotype.Service; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; @Service public class ProductService { private final ProductRepository productRepository; private final CacheService cacheService; public ProductService(ProductRepository productRepository, CacheService cacheService) { this.productRepository = productRepository; this.cacheService = cacheService; } // Example 1: Basic fallback with default value @CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback") public Product getProductById(Long id) { return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); } // Fallback method for basic scenario private Product getProductFallback(Long id, Exception exception) { log.warn("Fallback triggered for product ID: {}, Exception: {}", id, exception.getMessage()); // Return default product return new Product(id, "Fallback Product", "Temporarily unavailable", 0.0, "default"); } // Example 2: Fallback with cache @CircuitBreaker(name = "productService", fallbackMethod = "getProductWithCacheFallback") @Retry(name = "productService", fallbackMethod = "getProductWithCacheFallback") public Product getProductWithCache(Long id) { // Try to get from primary source return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); } private Product getProductWithCacheFallback(Long id, Exception exception) { log.warn("Using cache fallback for product ID: {}", id); // Try to get from cache return cacheService.getProductFromCache(id) .orElseGet(() -> createDefaultProduct(id)); } // Example 3: Multiple fallback methods for different exceptions @CircuitBreaker(name = "productService", fallbackMethod = "fallbackForTimeout") @Retry(name = "productService") public Product getProductWithDetailedFallbacks(Long id) { return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); } // Fallback for timeout exceptions private Product fallbackForTimeout(Long id, TimeoutException exception) { log.warn("Timeout fallback for product ID: {}", id); return cacheService.getProductFromCache(id) .orElse(createDefaultProduct(id)); } // Fallback for circuit breaker open private Product fallbackForTimeout(Long id, CallNotPermittedException exception) { log.warn("Circuit breaker open for product ID: {}", id); return cacheService.getProductFromCache(id) .orElseThrow(() -> new ServiceUnavailableException("Service temporarily unavailable")); } // Fallback for general exceptions private Product fallbackForTimeout(Long id, Exception exception) { log.error("General fallback for product ID: {}", id, exception); return createDefaultProduct(id); } private Product createDefaultProduct(Long id) { return new Product(id, "Default Product", "Product information temporarily unavailable", 0.0, "default"); } }

Advanced Fallback Patterns

1. Reactive Fallback Methods

import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; import io.github.resilience4j.reactor.retry.RetryOperator; @Service public class ReactiveProductService { private final ReactiveProductRepository productRepository; private final CacheService cacheService; private final CircuitBreaker circuitBreaker; private final Retry retry; public ReactiveProductService(ReactiveProductRepository productRepository, CacheService cacheService, CircuitBreakerRegistry circuitBreakerRegistry, RetryRegistry retryRegistry) { this.productRepository = productRepository; this.cacheService = cacheService; this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("productService"); this.retry = retryRegistry.retry("productService"); } // Reactive with fallback public Mono<Product> getProductReactive(Long id) { return productRepository.findById(id) .transformDeferred(CircuitBreakerOperator.of(circuitBreaker)) .transformDeferred(RetryOperator.of(retry)) .onErrorResume(throwable -> fallbackReactive(id, throwable)); } private Mono<Product> fallbackReactive(Long id, Throwable throwable) { log.warn("Reactive fallback for product ID: {}", id, throwable); return cacheService.getProductFromCacheReactive(id) .switchIfEmpty(Mono.defer(() -> Mono.just(createDefaultProduct(id)))); } // Flux with fallback public Flux<Product> getProductsByCategoryReactive(String category) { return productRepository.findByCategory(category) .transformDeferred(CircuitBreakerOperator.of(circuitBreaker)) .onErrorResume(throwable -> fallbackForCategoryReactive(category, throwable)); } private Flux<Product> fallbackForCategoryReactive(String category, Throwable throwable) { log.warn("Reactive fallback for category: {}", category, throwable); return cacheService.getProductsByCategoryReactive(category) .switchIfEmpty(Flux.defer(() -> Flux.just(createDefaultProduct(1L)) // Return at least one default )); } // Timeout with fallback public Mono<Product> getProductWithTimeout(Long id) { return productRepository.findById(id) .timeout(Duration.ofSeconds(5)) .transformDeferred(CircuitBreakerOperator.of(circuitBreaker)) .onErrorResume(TimeoutException.class, throwable -> { log.warn("Timeout for product ID: {}", id); return cacheService.getProductFromCacheReactive(id); }) .onErrorResume(throwable -> { log.error("Error fetching product ID: {}", id, throwable); return Mono.just(createDefaultProduct(id)); }); } }

2. Composite Fallback Strategies

@Service public class CompositeFallbackService { private final ProductRepository primaryRepository; private final ProductRepository secondaryRepository; private final CacheService cacheService; private final EventPublisher eventPublisher; public CompositeFallbackService(ProductRepository primaryRepository, ProductRepository secondaryRepository, CacheService cacheService, EventPublisher eventPublisher) { this.primaryRepository = primaryRepository; this.secondaryRepository = secondaryRepository; this.cacheService = cacheService; this.eventPublisher = eventPublisher; } // Multi-level fallback strategy @CircuitBreaker(name = "productService", fallbackMethod = "fallbackToSecondary") @Retry(name = "productService") public Product getProductWithMultiLevelFallback(Long id) { return primaryRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found in primary: " + id)); } // First fallback: Try secondary repository private Product fallbackToSecondary(Long id, Exception exception) { log.warn("Falling back to secondary repository for product ID: {}", id); eventPublisher.publishFallbackEvent("primary_to_secondary", id, exception); try { return secondaryRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found in secondary: " + id)); } catch (Exception secondaryException) { // If secondary also fails, go to next fallback return fallbackToCache(id, secondaryException); } } // Second fallback: Try cache private Product fallbackToCache(Long id, Exception exception) { log.warn("Falling back to cache for product ID: {}", id); eventPublisher.publishFallbackEvent("secondary_to_cache", id, exception); return cacheService.getProductFromCache(id) .orElseGet(() -> fallbackToDefault(id, exception)); } // Final fallback: Return default value private Product fallbackToDefault(Long id, Exception exception) { log.error("Using default fallback for product ID: {}", id, exception); eventPublisher.publishFallbackEvent("cache_to_default", id, exception); return createDefaultProduct(id); } // Fallback with recovery action @CircuitBreaker(name = "inventoryService", fallbackMethod = "updateInventoryFallback") public void updateInventory(Long productId, Integer quantity) { primaryRepository.updateInventory(productId, quantity); } private void updateInventoryFallback(Long productId, Integer quantity, Exception exception) { log.warn("Inventory update failed for product ID: {}, queueing for retry", productId); // Queue the update for later retry eventPublisher.publishRetryEvent("inventory_update", productId, quantity); // Update cache immediately cacheService.updateCachedInventory(productId, quantity); } }

3. Conditional Fallback Logic

@Service public class ConditionalFallbackService { private final ProductRepository productRepository; private final FeatureFlagService featureFlagService; private final MetricsService metricsService; public ConditionalFallbackService(ProductRepository productRepository, FeatureFlagService featureFlagService, MetricsService metricsService) { this.productRepository = productRepository; this.featureFlagService = featureFlagService; this.metricsService = metricsService; } @CircuitBreaker(name = "productService", fallbackMethod = "conditionalFallback") @Retry(name = "productService") public Product getProductWithConditionalFallback(Long id) { return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); } private Product conditionalFallback(Long id, Exception exception) { metricsService.incrementCounter("fallback.triggered", "product", id.toString()); // Different fallback strategies based on exception type if (exception instanceof TimeoutException) { return handleTimeoutFallback(id, (TimeoutException) exception); } else if (exception instanceof CallNotPermittedException) { return handleCircuitBreakerFallback(id, (CallNotPermittedException) exception); } else if (exception instanceof ProductNotFoundException) { return handleNotFoundFallback(id, (ProductNotFoundException) exception); } else { return handleGenericFallback(id, exception); } } private Product handleTimeoutFallback(Long id, TimeoutException exception) { log.warn("Timeout fallback for product ID: {}", id); if (featureFlagService.isEnabled("extended_timeout_fallback")) { // Try with extended timeout return productRepository.findByIdWithTimeout(id, Duration.ofSeconds(10)) .orElseGet(() -> createDefaultProduct(id)); } else { return createDefaultProduct(id); } } private Product handleCircuitBreakerFallback(Long id, CallNotPermittedException exception) { log.warn("Circuit breaker fallback for product ID: {}", id); // Check if we should use aggressive fallback if (featureFlagService.isEnabled("aggressive_circuit_breaker_fallback")) { return createAggressiveFallbackProduct(id); } else { return createDefaultProduct(id); } } private Product handleNotFoundFallback(Long id, ProductNotFoundException exception) { log.warn("Not found fallback for product ID: {}", id); // Try to find similar products or suggestions if (featureFlagService.isEnabled("product_suggestions")) { return findSimilarProduct(id) .orElseGet(() -> createDefaultProduct(id)); } else { return createDefaultProduct(id); } } private Product handleGenericFallback(Long id, Exception exception) { log.error("Generic fallback for product ID: {}", id, exception); // Log detailed error for analysis metricsService.recordError("fallback.generic_error", exception); return createDefaultProduct(id); } private Optional<Product> findSimilarProduct(Long originalId) { // Implementation to find similar products return Optional.empty(); } private Product createAggressiveFallbackProduct(Long id) { return new Product(id, "Service Temporarily Unavailable", "Please try again later", 0.0, "out-of-stock"); } }

Fallback with External Services

1. Payment Service Fallback Example

@Service public class PaymentService { private final PaymentGateway primaryGateway; private final PaymentGateway secondaryGateway; private final PaymentRepository paymentRepository; private final NotificationService notificationService; public PaymentService(PaymentGateway primaryGateway, PaymentGateway secondaryGateway, PaymentRepository paymentRepository, NotificationService notificationService) { this.primaryGateway = primaryGateway; this.secondaryGateway = secondaryGateway; this.paymentRepository = paymentRepository; this.notificationService = notificationService; } @CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback") @Retry(name = "paymentService", fallbackMethod = "processPaymentFallback") @TimeLimiter(name = "paymentService") public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) { return CompletableFuture.supplyAsync(() -> { // Validate request validatePaymentRequest(request); // Process with primary gateway PaymentResult result = primaryGateway.processPayment(request); // Save to database paymentRepository.save(result.toPaymentEntity()); return result; }); } private PaymentResult processPaymentFallback(PaymentRequest request, Exception exception) { log.warn("Payment fallback triggered for order: {}", request.getOrderId(), exception); // Strategy 1: Try secondary payment gateway if (shouldTrySecondaryGateway(exception)) { try { PaymentResult result = secondaryGateway.processPayment(request); paymentRepository.save(result.toPaymentEntity()); notificationService.notifyFallbackUsed("secondary_gateway", request.getOrderId()); return result; } catch (Exception secondaryException) { log.error("Secondary gateway also failed for order: {}", request.getOrderId(), secondaryException); } } // Strategy 2: Queue for later processing if (shouldQueuePayment(exception)) { paymentRepository.saveAsPending(request); notificationService.notifyPaymentQueued(request.getOrderId()); return PaymentResult.queued(request.getOrderId()); } // Strategy 3: Return failure with retry instructions return handleCompleteFailure(request, exception); } private boolean shouldTrySecondaryGateway(Exception exception) { return !(exception instanceof InvalidPaymentException) && !(exception instanceof InsufficientFundsException); } private boolean shouldQueuePayment(Exception exception) { return exception instanceof TimeoutException || exception instanceof CallNotPermittedException || exception instanceof NetworkException; } private PaymentResult handleCompleteFailure(PaymentRequest request, Exception exception) { log.error("Payment completely failed for order: {}", request.getOrderId(), exception); notificationService.notifyPaymentFailed(request.getOrderId(), exception.getMessage()); if (exception instanceof InvalidPaymentException) { return PaymentResult.failed(request.getOrderId(), "Invalid payment details"); } else if (exception instanceof InsufficientFundsException) { return PaymentResult.failed(request.getOrderId(), "Insufficient funds"); } else { return PaymentResult.failed(request.getOrderId(), "Payment service unavailable"); } } private void validatePaymentRequest(PaymentRequest request) { if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new InvalidPaymentException("Invalid payment amount"); } if (request.getCurrency() == null) { throw new InvalidPaymentException("Currency is required"); } } // Batch processing with fallback @CircuitBreaker(name = "batchPaymentService", fallbackMethod = "processBatchPaymentsFallback") public BatchPaymentResult processBatchPayments(List<PaymentRequest> requests) { List<CompletableFuture<PaymentResult>> futures = requests.stream() .map(this::processPayment) .collect(Collectors.toList()); CompletableFuture<Void> allFutures = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) ); return allFutures.thenApply(v -> { List<PaymentResult> results = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); return new BatchPaymentResult(results); }).join(); } private BatchPaymentResult processBatchPaymentsFallback(List<PaymentRequest> requests, Exception exception) { log.warn("Batch payment fallback triggered for {} requests", requests.size()); // Process each payment individually with fallbacks List<PaymentResult> results = requests.stream() .map(request -> { try { return processPayment(request).join(); } catch (Exception e) { return processPaymentFallback(request, e); } }) .collect(Collectors.toList()); return new BatchPaymentResult(results); } }

2. Notification Service Fallback

@Service public class NotificationService { private final EmailService emailService; private final SMSService smsService; private final PushNotificationService pushService; private final NotificationRepository notificationRepository; public NotificationService(EmailService emailService, SMSService smsService, PushNotificationService pushService, NotificationRepository notificationRepository) { this.emailService = emailService; this.smsService = smsService; this.pushService = pushService; this.notificationRepository = notificationRepository; } @CircuitBreaker(name = "notificationService", fallbackMethod = "sendNotificationFallback") @Retry(name = "notificationService") public void sendNotification(NotificationRequest request) { switch (request.getType()) { case EMAIL: emailService.sendEmail(request.getRecipient(), request.getSubject(), request.getMessage()); break; case SMS: smsService.sendSMS(request.getRecipient(), request.getMessage()); break; case PUSH: pushService.sendPush(request.getRecipient(), request.getSubject(), request.getMessage()); break; default: throw new IllegalArgumentException("Unsupported notification type: " + request.getType()); } // Mark as sent notificationRepository.markAsSent(request.getId()); } private void sendNotificationFallback(NotificationRequest request, Exception exception) { log.warn("Notification fallback for request: {}, type: {}", request.getId(), request.getType()); // Strategy 1: Retry with alternative channel if (tryAlternativeChannel(request)) { return; } // Strategy 2: Save for later retry notificationRepository.saveForRetry(request); // Strategy 3: Log and continue log.error("Failed to send notification: {}", request.getId(), exception); } private boolean tryAlternativeChannel(NotificationRequest request) { try { switch (request.getType()) { case EMAIL: // If email fails, try SMS smsService.sendSMS(request.getRecipient(), "Important: Please check your email for: " + request.getSubject()); log.info("Used SMS fallback for email notification: {}", request.getId()); return true; case SMS: // If SMS fails, try push notification pushService.sendPush(request.getRecipient(), "SMS Failed", request.getMessage()); log.info("Used push fallback for SMS notification: {}", request.getId()); return true; case PUSH: // If push fails, try email emailService.sendEmail(request.getRecipient(), "Push Notification", request.getMessage()); log.info("Used email fallback for push notification: {}", request.getId()); return true; default: return false; } } catch (Exception fallbackException) { log.warn("Alternative channel also failed for notification: {}", request.getId(), fallbackException); return false; } } }

Testing Fallback Methods

1. Unit Tests for Fallbacks

import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ProductServiceTest { @Mock private ProductRepository productRepository; @Mock private CacheService cacheService; @InjectMocks private ProductService productService; @Test void testFallbackWhenPrimaryServiceFails() { // Arrange Long productId = 1L; when(productRepository.findById(productId)) .thenThrow(new RuntimeException("Database unavailable")); when(cacheService.getProductFromCache(productId)) .thenReturn(Optional.of(createDefaultProduct(productId))); // Act Product result = productService.getProductWithCache(productId); // Assert assertNotNull(result); assertEquals(productId, result.getId()); assertEquals("Fallback Product", result.getName()); verify(cacheService).getProductFromCache(productId); } @Test void testMultipleFallbackLevels() { // Arrange Long productId = 1L; when(productRepository.findById(productId)) .thenThrow(new TimeoutException("Primary timeout")); when(cacheService.getProductFromCache(productId)) .thenReturn(Optional.empty()); // Act Product result = productService.getProductWithMultiLevelFallback(productId); // Assert assertNotNull(result); assertEquals("Default Product", result.getName()); } @Test void testCircuitBreakerFallback() { // Arrange Long productId = 1L; when(productRepository.findById(productId)) .thenThrow(CallNotPermittedException.createCallNotPermittedException( CircuitBreaker.of("test", CircuitBreakerConfig.ofDefaults()))); // Act Product result = productService.getProductWithDetailedFallbacks(productId); // Assert assertNotNull(result); assertEquals("Service temporarily unavailable", result.getDescription()); } @Test void testReactiveFallback() { // Arrange Long productId = 1L; when(productRepository.findByIdReactive(productId)) .thenReturn(Mono.error(new RuntimeException("Service down"))); when(cacheService.getProductFromCacheReactive(productId)) .thenReturn(Mono.just(createDefaultProduct(productId))); // Act & Assert StepVerifier.create(productService.getProductReactive(productId)) .expectNextMatches(product -> product.getId().equals(productId)) .verifyComplete(); } private Product createDefaultProduct(Long id) { return new Product(id, "Default Product", "Description", 0.0, "default"); } }

2. Integration Tests

import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @SpringBootTest class FallbackIntegrationTest { @MockBean private ProductRepository productRepository; @Autowired private ProductService productService; @Autowired private CircuitBreakerRegistry circuitBreakerRegistry; @Test void testCircuitBreakerWithFallback() { // Arrange - force circuit breaker to open state CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("productService"); circuitBreaker.transitionToOpenState(); when(productRepository.findById(anyLong())) .thenReturn(Optional.of(new Product())); // Act - should use fallback due to open circuit breaker Product result = productService.getProductById(1L); // Assert assertEquals("Fallback Product", result.getName()); // Reset circuit breaker circuitBreaker.transitionToClosedState(); } }

Best Practices and Patterns

1. Fallback Design Principles

@Component public class FallbackBestPractices { // 1. Keep fallbacks simple and fast @CircuitBreaker(name = "simpleService", fallbackMethod = "simpleFallback") public String getSimpleData() { return complexService.getData(); } private String simpleFallback(Exception e) { return "default-value"; // Simple, fast, no dependencies } // 2. Make fallbacks non-blocking @CircuitBreaker(name = "asyncService", fallbackMethod = "asyncFallback") public CompletableFuture<String> getAsyncData() { return CompletableFuture.supplyAsync(() -> externalService.getData()); } private CompletableFuture<String> asyncFallback(Exception e) { return CompletableFuture.completedFuture("default-async-value"); } // 3. Use appropriate fallback strategies public Object strategyPatternFallback(ServiceRequest request, Exception e) { FallbackStrategy strategy = selectFallbackStrategy(e); return strategy.execute(request); } private FallbackStrategy selectFallbackStrategy(Exception e) { if (e instanceof TimeoutException) { return new RetryWithBackoffStrategy(); } else if (e instanceof CircuitBreakerOpenException) { return new CacheFallbackStrategy(); } else { return new DefaultValueStrategy(); } } // 4. Monitor fallback usage @CircuitBreaker(name = "monitoredService", fallbackMethod = "monitoredFallback") public String getMonitoredData() { return externalService.getData(); } private String monitoredFallback(String param, Exception e) { metrics.incrementCounter("fallback.monitoredService"); log.warn("Fallback triggered for monitored service with param: {}", param); return "fallback-value"; } // 5. Test fallbacks thoroughly public void validateFallbackLogic() { // Always test: // - Normal execution // - Fallback execution // - Multiple fallback levels // - Different exception types // - Resource cleanup in fallbacks } }

2. Common Pitfalls and Solutions

@Component public class FallbackPitfalls { // Pitfall 1: Fallback that can also fail @CircuitBreaker(name = "service", fallbackMethod = "fallbackThatFails") public String getData() { return externalService.getData(); } // BAD: Fallback calls another external service private String fallbackThatFails(Exception e) { return anotherExternalService.getData(); // This can also fail! } // GOOD: Use local fallback private String goodFallback(Exception e) { return "local-fallback-value"; } // Pitfall 2: Fallback with side effects @CircuitBreaker(name = "service", fallbackMethod = "fallbackWithSideEffects") public void updateData(String data) { repository.save(data); } // BAD: Fallback has irreversible side effects private void fallbackWithSideEffects(String data, Exception e) { emailService.sendAlert("Update failed: " + data); // Irreversible! // This should be in the main method, not fallback } // GOOD: Fallback only for recovery private void goodFallback(String data, Exception e) { // Queue for retry instead of immediate action retryQueue.add(data); } // Pitfall 3: Ignoring exception context @CircuitBreaker(name = "service", fallbackMethod = "ignorantFallback") public String getDataWithContext() { return externalService.getData(); } // BAD: Ignoring exception type private String ignorantFallback(Exception e) { return "fallback"; // Same fallback for all exceptions } // GOOD: Context-aware fallback private String contextAwareFallback(Exception e) { if (e instanceof ValidationException) { return "invalid-input-fallback"; } else if (e instanceof TimeoutException) { return "timeout-fallback"; } else { return "generic-fallback"; } } }

This comprehensive guide covers fallback methods in Resilience4j, including basic usage, advanced patterns, testing strategies, and best practices for building resilient Java applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper