DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Designing Microservices Architecture With a Custom Spring Boot Starter and Auto-Configuration Framework
  • Challenges of Using Nginx in a Microservices Architecture
  • Integrating Spring Boot Microservices With MySQL Container Using Docker Desktop
  • Beyond REST: Architecting High-Density Agentic Microservices With MCP and WASI-NN

Trending

  • Build a GitHub Slack Bot With AWS Bedrock and MCP, Part 2
  • How to Interpret the Number of Spring ApplicationContexts in Integration Tests
  • Operationalizing Enterprise AI at Scale: Architecture, Governance, and Adoption
  • Build a GitHub Slack Bot With AWS Bedrock and MCP, Part 1
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. Effective Exception Handling in Microservices Integration

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.

By 
RENJITH RAMACHANDRAN user avatar
RENJITH RAMACHANDRAN
·
Dec. 31, 24 · Analysis
Likes (4)
Comment
Save
Tweet
Share
8.5K Views

Join the DZone community and get the full member experience.

Join For Free

Microservices 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:

Java
 
@PostMapping("/register")
    public ResponseEntity<String> registerUser(@RequestBody User user) {
        userRegistrationService.registerUser(user);
        return new ResponseEntity<>("User registered successfully", HttpStatus.CREATED);
    }


Code Snippet 2:

Java
 
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;
    }

Code Snippet 1 illustrates the controller code, which uses dependency injection to autowire and call the service layer. The 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.

Figure 1

Figure 1

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:

Java
 
@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);
    }
}

Figure 2

Figure 2

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.

Figure 3

Figure 3

DemoException


Code Snippet 4:
Java
 
package com.example.demo.exceptionhandling.exception;

public class DemoException extends RuntimeException {
    private String errorCode;

    public DemoException(String errorCode) {
        super(errorCode);
    }
    public DemoException(){
        super();
    }
}


Code Snippet 5:
Java
 
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:

Java
 
 @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);
    }


Figure 4

Figure 4


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.

Architecture Spring Boot Integration microservices

Opinions expressed by DZone contributors are their own.

Related

  • Designing Microservices Architecture With a Custom Spring Boot Starter and Auto-Configuration Framework
  • Challenges of Using Nginx in a Microservices Architecture
  • Integrating Spring Boot Microservices With MySQL Container Using Docker Desktop
  • Beyond REST: Architecting High-Density Agentic Microservices With MCP and WASI-NN

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook