Techniques for Robust Error Handling in Spring Boot Microservices
In microservices architecture, error handling is complex but essential to create resilient applications that can gracefully respond to unexpected issues. Spring Boot provides powerful tools to implement robust error-handling mechanisms, allowing ...

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse("RESOURCE_NOT_FOUND", ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequest(BadRequestException ex) {
ErrorResponse errorResponse = new ErrorResponse("BAD_REQUEST", ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse("INTERNAL_SERVER_ERROR", "An unexpected error occurred.");
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- Custom Error Messages: Each error has a specific ErrorResponse object, ensuring the client receives a clear and consistent error message.
- Granular Error Types: Different handlers for exceptions like ResourceNotFoundException and BadRequestException ensure that each error type has an appropriate HTTP status code and message.
- Fallback for Unhandled Exceptions: A generic handler catches unexpected errors, preserving system stability by hiding internal details and returning a user-friendly message.
public class ErrorResponse {
private String errorCode;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
this.timestamp = LocalDateTime.now();
}
// Getters and Setters
}
import io.github.resilience4j.retry.annotation.Retry;
@RestController
public class PaymentController {
@Retry(name = "paymentService", fallbackMethod = "paymentFallback")
@GetMapping("/processPayment")
public ResponseEntity<String> processPayment() {
// Call to a potentially failing external service
return new ResponseEntity<>(paymentService.process(), HttpStatus.OK);
}
public ResponseEntity<String> paymentFallback(Exception e) {
return new ResponseEntity<>("Payment Service is currently unavailable. Please try again later.", HttpStatus.SERVICE_UNAVAILABLE);
}
}
- Retry Annotation: The @Retry annotation enables retry logic on the processPayment method, making multiple attempts before invoking the fallback.
- Fallback Method: If the service fails after all retry attempts, paymentFallback provides a meaningful response to the client.
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
@RestController
public class InventoryController {
@CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")
@GetMapping("/checkInventory")
public ResponseEntity<String> checkInventory() {
return new ResponseEntity<>(inventoryService.checkAvailability(), HttpStatus.OK);
}
public ResponseEntity<String> inventoryFallback(Exception e) {
return new ResponseEntity<>("Inventory Service is temporarily unavailable.", HttpStatus.SERVICE_UNAVAILABLE);
}
}
- Circuit Breaker Annotation: This prevents repeated attempts when inventoryService experiences issues, protecting other services from being affected.
- Fallback Method: Returns a message indicating that the service is unavailable, giving a controlled response instead of cascading failures.
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import java.util.concurrent.CompletableFuture;
@RestController
public class OrderController {
@TimeLimiter(name = "orderService", fallbackMethod = "orderFallback")
@GetMapping("/placeOrder")
public CompletableFuture<String> placeOrder() {
return CompletableFuture.supplyAsync(() -> orderService.processOrder());
}
public CompletableFuture<String> orderFallback(Exception e) {
return CompletableFuture.completedFuture("Order Service took too long to respond. Please try again.");
}
}
- @TimeLimiter Annotation: Sets a timeout for placeOrder to avoid hanging if orderService takes too long.
- Fallback Method: Returns a message if the request exceeds the timeout, maintaining a seamless experience without indefinite waits.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class ProductService {
private static final Logger logger = LoggerFactory.getLogger(ProductService.class);
public Product getProductById(String id) {
try {
// Fetch product logic
} catch (ProductNotFoundException ex) {
logger.error("Product not found with id: {}", id, ex);
throw ex;
}
}
}
- Structured Error Information: Logs include specific details like product id and exception details, improving visibility.
- Error Consistency: By logging exceptions consistently, developers can trace issues across distributed services effectively.
Read more at : Techniques for Robust Error Handling in Spring Boot Microservices
About the author
I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
Comments
Join the discussionNo comments yet. Be the first to comment.





