Effective Exception Handling in Microservices Integration
Effectively handle errors in a Spring Boot microservices application with custom exceptions, ensuring meaningful error responses and seamless integration.
Join the DZone community and get the full member experience.
Join For FreeMicroservices architecture offers benefits such as scalability, agility, and maintainability, making it ideal for building robust applications. Spring Boot, as the preferred framework for developing microservices, provides various mechanisms to simplify integration with different systems. The modules offered by the Spring framework abstract much of the complexity, allowing developers to integrate seamlessly with external systems.
Integration types may vary depending on the system, including API integration, messaging system integration, or database connectivity. Each system requires specific error-handling mechanisms. Regardless of the integration type, the API layer should not directly expose errors returned by the integrated systems to ensure a consistent and user-friendly response.
Error Handling in a Sample Spring Boot Application
Below is an example of a Spring Boot application with a /register
API call for user registration. This API demonstrates integration with a database to save user details, an internal messaging system to post messages, and an external API.
Code Snippet 1:
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestBody User user) {
userRegistrationService.registerUser(user);
return new ResponseEntity<>("User registered successfully", HttpStatus.CREATED);
}
Code Snippet 2:
public void registerUser(User user) {
saveUserEntity(user);
registerEvents(user);
invokeLoginApi(user);
}
public ResponseEntity<String> invokeLoginApi(User user) {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername(user.getUsername());
loginDTO.setPassword(user.getPassword());
String url = "http://localhost:8080/api/auth/login";
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
HttpEntity<LoginDTO> request = new HttpEntity<>(loginDTO, headers);
return restTemplate.exchange(url, HttpMethod.POST, request, String.class);
}
private UserEntity saveUserEntity(User user) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(user.getUsername());
userEntity.setFirstName(user.getFirstName());
userEntity.setLastName(user.getLastName());
userEntity.setEmail(user.getEmail());
userRegistrationRepository.save(userEntity);
LoginDTO loginDTO = saveLoginDTO(user);
return userEntity;
}
private User registerEvents(User user) {
UserRegisteredEvent userRegisteredEvent = new UserRegisteredEvent();
userRegisteredEvent.setEmail(user.getEmail());
userRegisteredEvent.setFirstName(user.getFirstName());
userRegisteredEvent.setLastName(user.getLastName());
userRegisteredEvent.setEmail(user.getEmail());
applicationEventPublisher.publishEvent(userRegisteredEvent);
return user;
}
register
function in the service layer performs three key operations: saving user information in the database, producing an event, and invoking the login API to authenticate the user.
If an error occurs during data saving, event publishing, or API invocation, the system will return a generic 500 error, as shown in Figure 1. This error is not informative, making it difficult for the invoking client to understand the root cause. Developers must rely on logs to identify and debug the issue.
Controller Advice
A Controller advice can handle these exceptions and return a meaningful error, which invoking clients can use, as shown in Code Snippet 3 and Figure 2.
Code Snippet 3:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception ex) {
return new ResponseEntity<>("An unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Each integration layer may encounter different types of errors, and it is crucial to return meaningful information to the invoking client so that appropriate messages can be displayed. Returning a generic error for all scenarios is not a good design practice.
The Approach
To handle errors effectively, custom exceptions should be defined for each integration layer. Exceptions specific to an integration layer should be caught and encapsulated within these custom exception classes. These exceptions can be grouped under a single custom exception or differentiated by introducing specific attributes, enabling the Controller Advice to return more detailed and meaningful error responses for each scenario.
Figure 3 below illustrates the implementation of different custom exception classes, designed to encapsulate exceptions from various integration layers. Each custom exception class extends a base class, DemoException
, which itself extends the RuntimeException
class. This hierarchical structure ensures a consistent approach to exception handling across all integration layers.
package com.example.demo.exceptionhandling.exception;
public class DemoException extends RuntimeException {
private String errorCode;
public DemoException(String errorCode) {
super(errorCode);
}
public DemoException(){
super();
}
}
public void registerUser(User user) throws DemoException {
saveUserEntity(user);
registerEvents(user);
invokeLoginApi(user);
}
public ResponseEntity<String> invokeLoginApi(User user) throws DemoAPIException {
try {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername(user.getUsername());
loginDTO.setPassword(user.getPassword());
String url = "http://localhost:8080/api/auth/login";
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
HttpEntity<LoginDTO> request = new HttpEntity<>(loginDTO, headers);
return restTemplate.exchange(url, HttpMethod.POST, request, String.class);
}catch (Exception e){
throw new DemoAPIException("API-001:Error while invoking API");
}
}
private UserEntity saveUserEntity(User user) throws DemoDataException {
try {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(user.getUsername());
userEntity.setFirstName(user.getFirstName());
userEntity.setLastName(user.getLastName());
userEntity.setEmail(user.getEmail());
userRegistrationRepository.save(userEntity);
LoginDTO loginDTO = saveLoginDTO(user);
return userEntity;
}catch (Exception e){
throw new DemoDataException("DATA-001:Error while saving user data");
}
}
private User registerEvents(User user) throws DemoDataException{
try {
UserRegisteredEvent userRegisteredEvent = new UserRegisteredEvent();
userRegisteredEvent.setEmail(user.getEmail());
userRegisteredEvent.setFirstName(user.getFirstName());
userRegisteredEvent.setLastName(user.getLastName());
userRegisteredEvent.setEmail(user.getEmail());
applicationEventPublisher.publishEvent(userRegisteredEvent);
return user;
}catch (Exception e){
throw new DemoEventException("EVENT-001:Error while sending the user data");
}
}
As illustrated in Code Snippet 5, each function throws exceptions specific to its functionality, allowing the errors to be handled in the ControllerAdvice
class. This enables the application to return detailed and specific error responses. Code Snippet 6 demonstrates the ControllerAdvice
code, which handles each exception individually. Figure 4 shows the formatted error response. Unlike the generic error shown in Figure 3, the new error response is more descriptive, enabling the client code to handle it more effectively.
Code Snippet 6:
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception ex) {
return new ResponseEntity<>("An unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(DemoException.class)
public ResponseEntity<String> handleDemoException(DemoException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(DemoDataException.class)
public ResponseEntity<String> handleDemoDataException(DemoDataException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(DemoAPIException.class)
public ResponseEntity<String> handleDemoAPIException(DemoAPIException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.SERVICE_UNAVAILABLE);
}
Conclusion
Proper handling of errors from different integration layers is essential when developing microservices. It provides interfacing applications with better visibility into the errors, allowing them to handle issues appropriately while preventing the exposure of implementation details and code-related information, which could pose security risks.
The code for the example above can be found in this link.
Opinions expressed by DZone contributors are their own.
Comments