Hello! Welcome to your next lesson on building production-ready microservices.
In our last session, we discussed how to manage environment-specific settings using Spring Profiles. This ensures your application is a portable artifact that can be deployed anywhere, a key principle of modern software development. Today, we'll continue on the theme of production readiness by tackling another critical aspect: how your service behaves when things go wrong.
When a client makes a request to your API and an error occurs—whether it's due to invalid input, a missing resource, or an unexpected server issue—simply returning a default error page with a stack trace is unprofessional, insecure, and unhelpful. It exposes internal implementation details and forces the client to guess what went wrong.
For the senior-level roles you're targeting, demonstrating that you can design and implement a clean, consistent, and secure error handling strategy is non-negotiable. This lesson will show you exactly how to do that.
By the end of this lesson, you will be able to implement standardized error responses across a service using global exception handlers (@ControllerAdvice).
1. The Problem: The Chaos of Unhandled Exceptions
By default, if an exception is thrown from your Spring Boot controller and not caught, Spring's default mechanism kicks in. This often results in a generic 500 Internal Server Error response with a verbose, and often unhelpful, JSON body. For the client, this is a dead end.
The most basic way to solve this is to wrap the logic in every single controller method with a try-catch block.
@GetMapping("/products/{id}")
public ResponseEntity<?> getProductById(@PathVariable Long id) {
try {
Product product = productService.findById(id);
return ResponseEntity.ok(product);
} catch (ProductNotFoundException e) {
// Create a custom error response object
ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND, "Product not found", e.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}
While this works, it introduces significant problems that you should be prepared to discuss in an interview:
- Code Duplication: The same
catchblock logic is repeated in every method, violating the DRY (Don't Repeat Yourself) principle. - Maintenance Nightmare: If you need to change the error response format, you have to update it in every single controller method across the entire application.
- Scattered Logic: Business logic becomes cluttered with error-handling boilerplate, making the code harder to read and maintain.
Master Exception Handling in Spring Boot: @ExceptionHandler & @ControllerAdvice Explained
To see this problem in action, let's watch a segment from the video 'Master Exception Handling in Spring Boot' by CodeSnippet. It clearly demonstrates the issues with the traditional try-catch approach.
Watch from 04:36 to 13:45. Pay close attention to how handling the same exception in a second API endpoint requires duplicating the entire try-catch block. This visualizes the core problem we need to solve.
2. A Centralized Solution: @RestControllerAdvice
Spring provides a powerful mechanism to centralize cross-cutting concerns like exception handling using an Aspect-Oriented Programming (AOP) approach. Instead of handling exceptions inside your controllers, you can define a global handler that intercepts exceptions thrown from any controller.
This is achieved with two key annotations:
@RestControllerAdvice: This annotation declares a class as a global exception handler for REST controllers. It's a specialized version of@ControllerAdvicethat combines it with@ResponseBody, ensuring the return value of handler methods is automatically serialized into the response body (typically as JSON).@ExceptionHandler: This annotation is placed on methods within a@RestControllerAdviceclass. It specifies which exception type the method is responsible for handling.
Let's look at the three main steps to create a production-grade global exception handler.
Step 1: Define a Standard Error Response DTO
Consistency is key. All your error responses should have the same, predictable structure. A simple Java record or class is perfect for this. This DTO (Data Transfer Object) ensures clients can parse all errors in a uniform way.
Mastering Global Exception Handling in Spring Boot
The article 'Mastering Global Exception Handling in Spring Boot' provides an excellent, production-ready example of an ErrorResponse DTO.
Read 'Step 2: Standardized Error Response DTO'. Note the fields included: timestamp, status, error, message, and path. These provide essential context for debugging.
Your ErrorResponse class might look like this:
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
// Constructors, Getters, and Setters
}
Step 2: Create Custom, Business-Specific Exceptions
While you can handle generic exceptions like IllegalArgumentException, it's much more expressive to create your own custom exceptions for specific business scenarios. This makes your code self-documenting.
Mastering Global Exception Handling in Spring Boot
Let's refer back to the same article to see how to create a simple custom exception.
Review 'Step 1: Create Custom Exception Classes'. A simple unchecked exception extending RuntimeException is often sufficient.
Example:
// In exception/ResourceNotFoundException.java
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Now, your service layer can simply throw new ResourceNotFoundException("Product with ID " + id + " not found.") without worrying about how it will be handled.
Step 3: Implement the Global Exception Handler
Now we tie it all together in a single class.
Master Exception Handling in Spring Boot: @ExceptionHandler & @ControllerAdvice Explained
The 'CodeSnippet' video demonstrates this evolution perfectly, starting from a local handler and moving to a global one.
Watch from 13:45 to 22:32. This segment walks you through: Using @ExceptionHandler in a single controller (a small improvement). The limitation when a second controller is added. The final solution: creating a GlobalExceptionHandler class with @ControllerAdvice.
Here is a complete, production-ready example combining these concepts.
// In exception/GlobalExceptionHandler.java
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
ex.getMessage(),
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Add more handlers for other exceptions here...
}
With this in place, you can remove all try-catch blocks from your controllers. The code becomes clean, and the error handling logic is centralized and reusable.
3. Understanding the Exception Handling Flow
When an exception is thrown, how does Spring decide which handler to use? It follows a clear hierarchy, which is essential to understand.

Test your understanding!
You have a ProductController and an OrderController. A ProductNotFoundException can be thrown from both. You implement a method handleProductNotFound annotated with @ExceptionHandler(ProductNotFoundException.class) inside the ProductController. Will this handler catch the exception when it's thrown from the OrderController? Why or why not? What is the correct way to handle this for all controllers?
Show answer
No, it will not. An @ExceptionHandler method defined within a controller is local to that controller. It will only handle exceptions thrown from methods within ProductController.
The correct way is to move the handleProductNotFound method into a separate class annotated with @RestControllerAdvice. This makes the handler global, allowing it to intercept exceptions from any controller in the application.
4. Advanced Topics for Production Readiness
To really impress in an interview, you should go beyond the basics. A robust global handler should also manage common framework exceptions and have a safety net.
Handling Validation Errors
Spring Boot uses the jakarta.validation API for request validation (e.g., @Valid, @Min, @NotEmpty). When validation fails, a MethodArgumentNotValidException is thrown. Your global handler should catch this to provide clean validation error messages.
Mastering Global Exception Handling in Spring Boot
The GlobalExceptionHandler in the dev.to article includes a perfect example for handling validation exceptions.
In 'Step 3: Global Exception Handler', examine the method that handles MethodArgumentNotValidException. Notice how it iterates through the field errors to create a descriptive, comma-separated error message.
Handling Default Spring MVC Errors
What about errors like a client sending a GET request to an endpoint that only supports POST (405 Method Not Allowed)? Spring has a base class, ResponseEntityExceptionHandler, that provides default handlers for these. By extending this class, you can override its methods to return your custom ErrorResponse DTO instead of Spring's default.
Global Exception Handling Spring Boot with Controller advice | Exception Handling in Spring Boot
This is a powerful technique for achieving total consistency. The video 'Global Exception Handling Spring Boot' by Code Decode shows exactly how to do this.
Watch from 18:04 to 21:29. The example shows how to override handleHttpRequestMethodNotSupported to return a custom message body instead of the default empty response.
The Catch-All Handler
Finally, what if an unexpected NullPointerException or other runtime exception occurs? You should never let its stack trace leak to the client. Always include a generic, catch-all handler as a safety net. This is also where you should add critical logging.
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(
Exception ex, HttpServletRequest request) {
// IMPORTANT: Log the full stack trace for debugging
// logger.error("An unexpected error occurred", ex);
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
"An unexpected error occurred. Please try again later.", // Generic message for the client
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
5. Pro-Tip for Interviews: ProblemDetail (RFC 7807)
To demonstrate you're current with the latest Spring ecosystem trends, you can mention ProblemDetail. Introduced in Spring Boot 3, ProblemDetail is an implementation of RFC 7807, which standardizes a machine-readable format for error responses in HTTP APIs.
Instead of creating your own ErrorResponse DTO, you can use Spring's built-in ProblemDetail class. This aligns your API with a widely accepted internet standard.
Returning Errors Using ProblemDetail in Spring Boot
The article 'Returning Errors Using ProblemDetail in Spring Boot' from GeeksforGeeks provides a concise walkthrough.
Read 'Step 5: Create the Global Exception Handler'. Compare the handler method with the one we created earlier. You'll see it's very similar, but it constructs and returns a ProblemDetail object instead of a custom DTO. Mentioning this shows you value standardization.
Conclusion
You now have a complete, production-grade strategy for handling exceptions in your microservices. This approach not only makes your code cleaner and more maintainable but also provides a professional and secure API for your clients.
Key Takeaways:
- Centralize with
@RestControllerAdvice: Avoidtry-catchblocks in controllers by using a global exception handler class. - Standardize Responses: Use a consistent error DTO (like a custom
ErrorResponseor the standardProblemDetail) for all error scenarios. This includes a timestamp, status code, and clear message. - Use Specific Exceptions: Create custom exceptions that extend
RuntimeExceptionfor clear, self-documenting business error conditions. - Be Comprehensive: Your handler should manage not only your custom exceptions but also common framework errors like validation failures (
MethodArgumentNotValidException) and have a catch-all handler for unexpected issues (Exception.class). - Never Leak Implementation Details: Always control the error message sent to the client and never expose raw stack traces.
In an interview, describing this layered approach—from custom exceptions to a global handler that covers business logic, validation, and a generic fallback—will demonstrate a deep understanding of robust API design.
Next Up
Our service is now configurable and handles errors gracefully. But a microservice rarely lives in isolation. It needs to communicate with other services. In our next lesson, we will begin Module 2 by exploring the fundamental choice you must make for every interaction: explaining the trade-offs between synchronous and asynchronous communication patterns.
Can't find a good explanation? Sign up and we'll make it for you
Sign up