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.