Spring Boot Actuator + Exception Handling
- What is Spring Boot Actuator ?
- Default Actuator endpoints (/actuator/health, /actuator/info, /actuator/metrics)
- How to enable/disable endpoints (management.endpoints.web.exposure.include)
- Custom health indicator (HealthIndicator interface)
- Custom info contributor (InfoContributor)
- Explain Metrics with Micrometer
- Explain custom metrics with micrometer
- What are common exception handling problems?
- What is @ControllerAdvice / @RestControllerAdvice?
- @ExceptionHandler — how it works ?
- Explain MethodArgumentNotValidException
- Explain ConstraintViolationException
- Custom exception classes
- ResponseEntityExceptionHandler
- Consistent error response format
- What are the best practices for Spring Boot Actuator + Exception handling ?
What is Spring Boot Actuator ?
=> Spring Boot Actuator is a production ready feature module that provides built-in endpoints for monitoring and managing the application while its running
=> It helps you to understand the health, metrics, environment and internal state of your app at runtime
Actuator key endpoints
| Endpoint | Path Example | Purpose / What It Shows |
|---|---|---|
| Health | /actuator/health | App is UP/DOWN + details (DB, disk, mail, etc.) |
| Info | /actuator/info | Custom info (git commit, build version, app name) |
| Metrics | /actuator/metrics | JVM memory, CPU, HTTP requests, DB connections, etc. |
| Loggers | /actuator/loggers | View/change log levels at runtime |
| Env | /actuator/env | Environment variables & properties |
| Beans | /actuator/beans | All Spring beans in context |
| Threaddump | /actuator/threaddump | Current thread dump |
| Heapdump | /actuator/heapdump | Download heap dump for analysis |
| Mappings | /actuator/mappings | All registered endpoints |
How to enable Actuator ?
=> Add dependency in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
=> Configuration (application.properties)
# Expose all endpoints over web
management.endpoints.web.exposure.include=*
# Or specific ones
management.endpoints.web.exposure.include=health,info,metrics
# Show detailed health (DB, disk, etc.)
management.endpoint.health.show-details=always
# Disable security for actuator (development only)
management.security.enabled=false
Security (Production)
=> By default, only /health and /info are exposed
For production,
=> use Spring Security to protect endpoints
=> Expose only needed ones (eg. health, metrics)
Custom Health Indicator (Example)
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Check DB connection
if (isDatabaseUp()) {
return Health.up().build();
}
return Health.down().withDetail("reason", "DB down").build();
}
}
Default Actuator endpoints (/actuator/health, /actuator/info, /actuator/metrics)
=> By default, only /health and /info are exposed, all others are disabled for security
=> To expose more (not recommended in production), do the changes in application.properties or application.yml
management.endpoints.web.exposure.include=health,info,metrics,loggers,env,beans,threaddump,heapdump
# Or expose all
management.endpoints.web.exposure.include=*
Most default key endpoints
| Endpoint | Full Path | Description & Purpose | Default Exposed? | Typical Response (JSON) |
|---|---|---|---|---|
| Health | /actuator/health | Shows if the application is healthy (UP/DOWN) + details of components (DB, disk, mail, etc.). | Yes | { "status": "UP", "components": { "db": { "status": "UP" } } } |
| Info | /actuator/info | Displays custom info about the app (build version, git commit, custom properties). | Yes | { "app": { "name": "MyApp", "version": "1.0.0" } } |
| Metrics | /actuator/metrics | Lists all available metrics (JVM memory, CPU, HTTP requests, DB connections, etc.). | No (by default) | List of metric names + details on /actuator/metrics/jvm.memory.used |
| Env | /actuator/env | Shows environment variables, system properties, and all loaded properties. | No | Full env dump (sensitive — secure in prod) |
| Beans | /actuator/beans | Lists all Spring beans in the application context. | No | JSON of beans with scope, dependencies, etc. |
| Loggers | /actuator/loggers | View and change log levels at runtime (e.g., set DEBUG for a package). | No | { "levels": ["OFF", "ERROR", ...], "loggers": { ... } } |
| Threaddump | /actuator/threaddump | Returns current thread dump (useful for deadlock diagnosis). | No | JSON thread dump |
| Heapdump | /actuator/heapdump | Downloads heap dump file for memory analysis (e.g., with VisualVM). | No | Binary heap dump file |
Best Practice (Production)
=> Never expose sensitive endpoints (env, beans, threaddump, heapdump) publicly
=> Use Spring Security to protect Actuator:
management.endpoints.web.base-path=/actuator
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=health,info
=> Enable authentication for /actuator/*
How to enable/disable endpoints (management.endpoints.web.exposure.include)
=> We can enable/disable endpoints via the configuration in application.properties or application.yml file
Expose only health and metrics
management.endpoints.web.exposure.include=health,metrics
Expose everything (development only!)
management.endpoints.web.exposure.include=*
Exclude sensitive ones
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=env,beans,threaddump,heapdump,loggers
YAML equivalent (application.yml)
management:
endpoints:
web:
exposure:
include: health,info,metrics
exclude: env,beans
Custom health indicator (HealthIndicator interface)
=> Custom health indicator in Spring Boot Actuator allows you to define your own health checks for the application (eg. database connectivity, external API availability, cache status, disk space, etc)
=> It can be implemented by the following steps
1. Create class implements HealthIndicator
2. Register the class as spring bean (use @Component at class level to register it as bean)
Health Indicator Interface
public interface HealthIndicator {
Health health();
}
=> health() method returns a Health object with Status and Details
=> Status: UP (healthy), DOWN (unhealthy), OUT_OF_SERVICE, UNKNOWN
=> Details: Optional key-value pairs (e.g., error message, version)
Step-by-Step Example: Database Health Indicator
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection connection = dataSource.getConnection()) {
if (connection.isValid(1)) {
return Health.up()
.withDetail("database", "Connected")
.withDetail("driver", connection.getMetaData().getDriverName())
.build();
}
} catch (SQLException e) {
return Health.down(e)
.withDetail("error", "Connection failed: " + e.getMessage())
.build();
}
return Health.down().withDetail("error", "Connection not valid").build();
}
}
Output on /actuator/health (when healthy)
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "Connected",
"driver": "H2 JDBC Driver"
}
},
"diskSpace": { ... }
}
}
Output when unhealthy
{
"status": "DOWN",
"components": {
"db": {
"status": "DOWN",
"details": {
"error": "Connection failed: ..."
}
}
}
}
How it works
=> Spring Boot auto-detects all beans implementing HealthIndicator
=> All health checks run when /actuator/health is called
=> Overall app health is UP only if all indicators are UP
Best Practices
=> Name it clearly: DatabaseHealthIndicator, CacheHealthIndicator, ApiHealthIndicator
=> Keep it fast: Health check should respond in < 1 second
=> Add details: Include useful info (version, connection string, error)
=> Use @Component: Spring auto-detects it
Why we need custom HealthIndicator ?
=> Without any custom HealthIndicator, Actuator already checks database connectivity, disk space, and other components
=> Custom HealthIndicator is only needed for additional checks (e.g., external API, custom cache, payment gateway)
=> Defining a custom HealthIndicator does NOT omit or replace the default built-in health checks
=> When you add your own custom HealthIndicator in Spring Boot Actuator, it is added to the list of existing health indicators. The /actuator/health endpoint will run ALL indicators (default ones + your custom ones) and combine their statuses
Custom info contributor (InfoContributor)
=> Custom info contributor in Spring Boot Actuator allows you to add your own custom information to the /actuator/info endpoint (eg. build details, git commit info, environment specific data, version, etc)
=> It can be implemented by the following steps
1. Create class implements InfoContributor
2. Register the class as spring bean (use @Component at class level to register it as bean)
Info Contributor Interface
public interface InfoContributor {
void contribute(Info.Builder builder);
}
=> You build key-value pairs using Info.Builder
Step-by-Step Example: AppInfoContributor to add application version & owner
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("appVersion", "1.2.3")
.withDetail("owner", "Team Funkynshot")
.withDetail("environment", "Development")
.withDetail("lastDeployed", "2026-01-10");
}
}
Output on /actuator/info
{
"appVersion": "1.2.3",
"owner": "Team Funkynshot",
"environment": "Development",
"lastDeployed": "2026-01-10"
}
More Useful Examples
1. Git Commit Info (Auto-populated if git info is available)
# application.properties
management.info.git.mode=full
//Then in code for the class AppInfoContributor
builder.withDetail("git", gitProperties); // gitProperties injected if present
2. Build Info (From Maven/Gradle)
@Component
public class BuildInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("build", Map.of(
"version", "1.0.0",
"timestamp", "2026-01-10T10:00:00Z"
));
}
}
3. Dynamic Info (e.g., external API status)
builder.withDetail("apiStatus", checkExternalApi() ? "UP" : "DOWN");
Best practices
=> Keep it fast — contribute() should return quickly
=> Use for static or slowly changing info (version, build time)
=> Avoid sensitive data — /info is often exposed
=> Combine with Git Properties plugin for auto git info
Explain Metrics with Micrometer
=> Metrics with Micrometer is used to collect, expose and monitor application metrics (performance counters, gauges, timers, etc) using the Micrometer library
Key points about Micrometer in Spring Boot
1. Auto Enabled
=> When we add the dependency spring-boot-starter-actuator, the micrometer is automatically enabled
=> Spring Boot registers default metrics (JVM memory, CPU, HTTP requests, DB connections, etc)
2. Main metric types
| Type | Purpose | Example Use Case |
|---|---|---|
| Counter | Counts occurrences (monotonically increasing) | Number of HTTP requests, errors |
| Gauge | Records a value that can go up/down | Current active users, queue size |
| Timer | Measures duration (latency, execution time) | API response time, DB query time |
| Distribution Summary | Tracks distribution of values (percentiles) | Response size, file size |
3. Default Metrics (Auto-Collected)
=> JVM: memory.used, threads.live, gc.pause
=> HTTP: http.server.requests (count, duration, percentiles)
=> Database: jdbc.connections.active, hikari.connections
=> Process: system.cpu.usage, process.uptime
4. How to expose metrics ?
=> In application.properties file,
management.endpoints.web.exposure.include=metrics
management.metrics.export.prometheus.enabled=true # For Prometheus
5. Custom Metrics (Example)
@Service
public class OrderService {
private final MeterRegistry registry;
public OrderService(MeterRegistry registry) {
this.registry = registry;
}
public void processOrder() {
// Counter
registry.counter("orders.processed").increment();
// Timer
Timer timer = registry.timer("orders.processing.time");
timer.record(() -> {
// long-running operation
Thread.sleep(100);
});
// Gauge (current value)
registry.gauge("orders.active", this, OrderService::getActiveOrdersCount);
}
private int getActiveOrdersCount() {
return activeOrders.size();
}
}
Explain custom metrics with micrometer
=> Custom Metrics with Micrometer in Spring Boot allow you to define and track your own application-specific metrics (e.g., number of orders placed, active users, API latency, error counts) in addition to the built-in ones (JVM, HTTP, DB).
Key Metric Types for Custom Metrics
| Type | Purpose | When to Use | Example |
|---|---|---|---|
| Counter | Counts occurrences (only increases) | Number of requests, errors, events | counter("orders.created").increment() |
| Gauge | Records a value that can go up/down | Current active users, queue size, cache hits | gauge("active.users", activeUsers.size()) |
| Timer | Measures duration/latency | API response time, DB query time | timer("api.latency").record(() -> longTask()) |
| DistributionSummary | Tracks distribution (percentiles, histograms) | Request sizes, response times | summary("response.size").record(1024) |
How to Add Custom Metrics (Step-by-Step)
1. Add dependency (already included if you have spring-boot-starter-actuator) in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2. Inject MeterRegistry (Micrometer's core)
@Service
public class OrderService {
private final MeterRegistry registry;
public OrderService(MeterRegistry registry) {
this.registry = registry;
}
}
3. Create custom metrics
Counter Example (e.g., count orders created)
public void createOrder(Order order) {
registry.counter("orders.created", "type", "online").increment(); // Tags for filtering
// ... business logic
}
Timer Example (measure order processing time)
public void processOrder(Order order) {
Timer timer = registry.timer("orders.processing.time", "type", "online");
timer.record(() -> {
// Simulate long work
Thread.sleep(500);
});
}
Gauge Example (current active orders)
private AtomicInteger activeOrders = new AtomicInteger(0);
public void startOrder() {
activeOrders.incrementAndGet();
registry.gauge("orders.active", activeOrders, AtomicInteger::get);
}
4. Expose metrics
=> In application.properties file,
management.endpoints.web.exposure.include=metrics
5. Access
http://localhost:8080/actuator/metrics/orders.created
http://localhost:8080/actuator/metrics/orders.processing.time
What are common exception handling problems?
Here are the most common exception handling problems in Spring Boot applications (especially in REST APIs):
01. Repetitive try-catch blocks in controllers
=> Every endpoint has its own try-catch → code duplication, hard to maintain, inconsistent error responses.
02. No global exception handling
=> Unhandled exceptions return default Spring error page (Whitelabel Error Page) → not user-friendly, leaks stack traces in production.
03. Exposing stack traces or internal details
=> Returning full exception stack trace to client → security risk (exposes code paths, server info)
04. Inconsistent error response format
=> Different endpoints return errors in different shapes (message only, status code only, custom objects) → hard for frontend/mobile to handle.
05. Not handling validation errors properly
=> @Valid failures throw MethodArgumentNotValidException → default ugly response instead of clean JSON with field errors
06. Swallowing exception
=> Catching Exception and doing nothing (or just logging) → silent failures, app continues in broken state
07. Using getCause() or printStackTrace() in responses
=> Exposing technical details to end users → bad UX, security issue
08. Not handling specific exceptions
=> Treating all exceptions as 500 → loses business context (e.g., 404 for not found, 400 for bad request)
09. Missing rollback in transactions
=> Custom exceptions not triggering rollback → partial DB changes
10. No logging for exceptions
=> Exceptions thrown but not logged → hard to debug in production
Best Practices
=> No try-catch in controllers (unless very specific case)
=> Keep Controllers thin :
Extract params/body
Call service
Return ResponseEntity
Example for clean controller :
@PostMapping("/employees")
public ResponseEntity<Employee> create(@Valid @RequestBody EmployeeDto dto) {
Employee saved = employeeService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
=> Throw exceptions from service layer
=> Use @RestControllerAdvice + @ExceptionHandler for global handling
=> Return consistent ErrorResponse DTO (status, message, timestamp, path, details)
=> Handle MethodArgumentNotValidException for validation
=> Create custom exceptions (e.g., ResourceNotFoundException, ValidationException)
=> Log exceptions with stack trace (use logger.error)
=> Use ResponseEntity for proper status codes
=> Never expose stack traces in production (use spring.profiles.active=prod to hide)
What is @ControllerAdvice / @RestControllerAdvice?
=> @ControllerAdvice and @RestControllerAdvice are used for global exception handling
=> We know the difference between @Controller and @RestController already. With the same logic, @RestControllerAdvice = @ControllerAdvice + @ResponseBody
=> @ControllerAdvice / @RestControllerAdvice is a specialized form of @Component that allows you to handle exceptions globally across all @Controller or @RestController classes in your Spring Boot application
=> One centralized place for all exception handling logic
| Feature | @ControllerAdvice | @RestControllerAdvice |
|---|---|---|
| Main Use | Traditional MVC (views/templates) | REST APIs (JSON/XML responses) |
| @ResponseBody | Manual (per method or class) | Automatic on all methods |
| Typical Return | View name (String), ModelAndView | ResponseEntity, Map, DTO |
| Common With | Thymeleaf, JSP | REST controllers, mobile/SPA clients |
How It Works
=> Spring scans for classes annotated with @ControllerAdvice / @RestControllerAdvice
=> Methods annotated with @ExceptionHandler inside it are invoked when the specified exception is thrown from any controller.
Basic Example – Global Exception Handler
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody (for REST)
public class GlobalExceptionHandler {
// Handle specific exception
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Handle validation errors (from @Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
// Fallback for any other exception
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Unexpected error: " + ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Simple error response DTO
record ErrorResponse(int status, String message, LocalDateTime timestamp) { }
Common Exceptions Handled
| Exception Type | Typical Status | Use Case |
|---|---|---|
ResourceNotFoundException | 404 | Entity not found |
MethodArgumentNotValidException | 400 | @Valid validation failures |
ConstraintViolationException | 400 | @Valid on method parameters |
DataIntegrityViolationException | 400/409 | DB constraint violation |
AccessDeniedException (Security) | 403 | Authorization failure |
Exception (fallback) | 500 | Unexpected errors |
Advanced Features
=> @Order — control precedence if multiple @ControllerAdvice.
=> @ExceptionHandler with multiple exceptions:
@ExceptionHandler({ResourceNotFoundException.class, IllegalArgumentException.class})
Best Practice
=> Place GlobalExceptionHandler in a separate package, not inside Model, Service, or Controller layers.
com.example.demo
├── config ← Configuration & global setup (Place GlobalExceptionHandler here)
│ ├── GlobalExceptionHandler.java
│ ├── SecurityConfig.java
│ └── WebConfig.java
├── controller ← REST endpoints (@RestController)
├── service ← Business logic (@Service)
├── repository ← JPA repositories
├── model ← Entities (@Entity) + DTOs
├── exception ← Custom exceptions (ResourceNotFoundException, etc.)
└── DemoApplication.java ← Main class
=> Use @RestControllerAdvice for REST APIs (most cases)
=> Return consistent error format (status, message, timestamp, path)
=> Handle common exceptions: MethodArgumentNotValidException, HttpMessageNotReadableException, custom business exceptions
=> Never expose stack traces in production
=> Log exceptions in handler (logger.error(...))
@ExceptionHandler — how it works ?
=> @ExceptionHandler is an annotation in Spring that tells a method in a @ControllerAdvice or @RestControllerAdvice class to handle specific exceptions thrown from any controller in the application.
=> Spring scans and registers this class annotated with @ControllerAdvice or @RestControllerAdvice globally
=> Spring checks if the exception type matches any @ExceptionHandler
=> If match found → Spring calls that method.
=> The exception object is passed as a parameter to the handler method
=> The handler returns a custom response (usually ResponseEntity or custom DTO)
=> Multiple exceptions can be handled in one method :
Eg. @ExceptionHandler({ResourceNotFoundException.class, IllegalArgumentException.class})
=> Fallback handler (catch-all) :
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(500, "Internal error", LocalDateTime.now()));
}
=> Works globally across all controllers
=> Basically, it is used to return uniform error format response
=> @RestControllerAdvice = returns JSON automatically (most common for REST)
=> @ControllerAdvice = for traditional MVC (returns view names)
Basic Example – Global Exception Handler
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody (for REST)
public class GlobalExceptionHandler {
// Handle specific exception
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// Handle validation errors (from @Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
// Fallback for any other exception
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Unexpected error: " + ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Simple error response DTO
record ErrorResponse(int status, String message, LocalDateTime timestamp) { }
Explain MethodArgumentNotValidException
=> Consider a handler method in Controller
=> In the controller handler method, the method parameter is marked with the annotation @valid (or @validated)
Example:
@PostMapping("/employees")
public ResponseEntity<?> create(@Valid @RequestBody EmployeeDto dto) {
// ...
}
=> @valid annotation tells to validate the method parameter ie. here in the example EmployeeDTO is using Bean Validation annotations (e.g., @NotBlank, @NotNull, @Size, @Email, @Positive on DTO fields)
=> When the request body is invalid (eg. name is blank), Spring throws MethodArgumentNotValidException before the controller method is called
Key points
=> Bean validation will happen only if we mark with @Valid (or @Validated) in the controller handler method
=> When the request body is invalid (eg. name is blank), Spring throws MethodArgumentNotValidException before the controller method is called
=> It will return the BindingResult exception object that holds all the validation errors (e.getBindingResult().getFieldErrors().)
How to handle the MethodArgumentNotValidException globally ?
=> Use @RestControllerAdvice to catch it and return a clean JSON response:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
Sample Error Response (when name is blank):
{
"name": "Name is required",
"salary": "Salary must be positive"
}
Explain ConstraintViolationException
=> Both MethodArgumentNotValidException and ConstraintViolationException are unchecked (runtime) exceptions
=> MethodArgumentNotValidException occurs when validation fails on @RequestBody, @RequestParam, or @PathVariable in controller methods.
=> ConstraintViolationException occurs when validation fails on method parameters in any bean (controller, service, etc.) using @Validated or manual Validator.validate().
Practical Difference with Examples
1. MethodArgumentNotValidException (Most Common – Controller)
@PostMapping("/employees")
public ResponseEntity<?> create(@Valid @RequestBody EmployeeDto dto) {
// ...
}
=> Invalid DTO (e.g., blank name) → throws MethodArgumentNotValidException
=> Handled by @ExceptionHandler(MethodArgumentNotValidException.class) method
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
2. ConstraintViolationException (Service or Method-Level)
@Service
@Validated
public class EmployeeService {
public void create(@NotBlank String name, @Positive int salary) {
// ...
}
}
=> Invalid params → throws ConstraintViolationException
=> Handled by @ExceptionHandler(ConstraintViolationException.class) method
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handle(ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(v ->
errors.put(v.getPropertyPath().toString(), v.getMessage())
);
return ResponseEntity.badRequest().body(errors);
}
| Aspect | MethodArgumentNotValidException | ConstraintViolationException |
|---|---|---|
| When it is thrown | When validation fails on @RequestBody, @RequestParam, or @PathVariable in controller methods. | When validation fails on method parameters in any bean (controller, service, etc.) using @Validated or manual Validator.validate(). |
| Most common scenario | @PostMapping with @Valid @RequestBody DTO in controller | @Validated on service method params or manual validation |
| Exception thrown by | Spring MVC (argument resolver) | Bean Validation (Jakarta Validation) |
| Contains | BindingResult (field errors from @Valid) e.getBindingResult().getFieldErrors() | Set<ConstraintViolation> (violations on parameters)e.getConstraintViolations() |
| Typical HTTP status | 400 Bad Request | 400 Bad Request (if handled) |
| Default handling in Spring Boot | Returns Whitelabel Error Page (HTML) | Throws 500 if not caught (or handled by advice) |
| Best handler | @ExceptionHandler(MethodArgumentNotValidException.class) | @ExceptionHandler(ConstraintViolationException.class) |
Custom exception classes
=> Custom exception classes means user defined exception classes
Why we use custom exception classes ?
=> Better readability
=> Consistent error responses
=> Exceptions carry meaningful messages and details
Types of Custom Exceptions
| Type | Base Class | Typical HTTP Status | Use Case Example |
|---|---|---|---|
| NotFoundException | RuntimeException | 404 Not Found | Resource not found (e.g., employee ID doesn't exist) |
| BadRequestException | RuntimeException | 400 Bad Request | Invalid input, validation failure |
| ConflictException | RuntimeException | 409 Conflict | Duplicate record, business rule violation |
| UnauthorizedException | RuntimeException | 401 Unauthorized | Authentication failure |
| ForbiddenException | RuntimeException | 403 Forbidden | Authorization failure |
| InternalServerException | RuntimeException | 500 Internal Server | Unexpected errors |
Best Practice Structure
1. Create a base custom exception (optional but recommended)
public abstract class ApiException extends RuntimeException {
private final HttpStatus status;
public ApiException(HttpStatus status, String message) {
super(message);
this.status = status;
}
public HttpStatus getStatus() {
return status;
}
}
2. Specific exceptions
public class ResourceNotFoundException extends ApiException {
public ResourceNotFoundException(String message) {
super(HttpStatus.NOT_FOUND, message);
}
}
public class ValidationException extends ApiException {
public ValidationException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
3. Throw in service
public Employee findById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id));
}
4. Model class/record for ErrorResponse
record ErrorResponse(int status, String message, LocalDateTime timestamp) { }
5. Global handling in @RestControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(ex.getStatus())
.body(new ErrorResponse(ex.getStatus().value(), ex.getMessage(), LocalDateTime.now()));
}
// Similar for other custom exceptions
}
ResponseEntityExceptionHandler
=> ResponseEntityExceptionHandler is a base class in Spring MVC for global exception handling
=> It provides default implementations for common Spring exceptions
=> It ensures consistent error responses for built-in exceptions
Key Built-in Handled Exceptions (with default status)
| Exception | Default Status | Typical Trigger |
|---|---|---|
| MethodArgumentNotValidException | 400 Bad Request | @Valid on @RequestBody fails |
| BindException | 400 Bad Request | Binding errors on form data |
| HttpMessageNotReadableException | 400 Bad Request | Invalid JSON in request body |
| HttpRequestMethodNotSupportedException | 405 Method Not Allowed | Wrong HTTP method |
| HttpMediaTypeNotSupportedException | 415 Unsupported Media Type | Wrong Content-Type |
| MissingServletRequestParameterException | 400 Bad Request | Required @RequestParam missing |
| ServletRequestBindingException | 400 Bad Request | Binding errors on request |
| NoHandlerFoundException | 404 Not Found | Endpoint not found (if enabled) |
How to Use It (Extend and Customize)
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Override specific exception handling
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
// Handle custom exceptions (not overridden)
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(404, ex.getMessage(), LocalDateTime.now()));
}
// Fallback for any unhandled
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(500, "Unexpected error", LocalDateTime.now()));
}
}
Consistent error response format
=> Consistent error response format means returning errors in a standard JSON structure (status, error, message, timestamp, path, details) for every failure.
=> Use @RestControllerAdvice + @ExceptionHandler to centralize the exception handling
=> Since the error response is in structured consistent error format, clients (frontend, mobile, other services) can reliably parse and display them.
What are the best practices for Spring Boot Actuator + Exception handling ?
Actuator Best Practices
1. Add DevTools in development only
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
→ Auto-restart, LiveReload — disabled in production.
2. Expose only necessary endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoints.web.exposure.exclude=env,beans,heapdump,threaddump,loggers
→ Never expose `*` in production.
3. Secure Actuator endpoints
- Use Spring Security (basic auth, JWT, OAuth).
- Or expose only `/health` and `/info`.
- Set `management.endpoint.health.show-details=when-authorized` (requires auth for details).
4. Enable detailed health
management.endpoint.health.show-details=always # Development
management.endpoint.health.show-details=when-authorized # Production
5. Add custom health indicators
- For DB, external APIs, cache — implement `HealthIndicator`.
- Keep checks fast (<1 sec).
6. Add custom info
- Implement `InfoContributor` for build/git/version info.
7. Use Micrometer for custom metrics
- Inject `MeterRegistry` → create counter/timer/gauge.
- Expose `/actuator/metrics`.
Exception Handling Best Practices
1. Use @RestControllerAdvice
- Centralize all error handling for REST APIs.
2. Return consistent error format
json
{
"status": 404,
"error": "Not Found",
"message": "Employee not found",
"timestamp": "2026-01-10T12:34:56",
"path": "/api/employees/123"
}
- Use a record or class `ErrorResponse`.
3. Handle common exceptions
- `MethodArgumentNotValidException` (validation)
- `HttpMessageNotReadableException` (invalid JSON)
- `NoHandlerFoundException` (404)
- Custom exceptions (`ResourceNotFoundException`, `ValidationException`)
4. Never expose stack traces in production
- Use `spring.profiles.active=prod` to hide details.
- Log stack trace internally (`logger.error(ex, ex)`).
5. Use ResponseEntity for responses
- Return proper status codes (404, 400, 201, 204).
6. Log exceptions
- Always log in handler: `logger.error("Error processing request", ex)`.
7. Custom exceptions for business errors
- `ResourceNotFoundException`, `BusinessRuleException`, etc.
- Throw from service, handle globally.
_____________________________________________________________________
Coding Practice :
Do the Setup
Add spring-boot-starter-actuator to pom.xml.
Add spring-boot-starter-validation (if not already).
Excercises :
- Add Actuator dependency → run → visit /actuator.
- Expose health and info: management.endpoints.web.exposure.include=health,info.
- Add custom info (build version, git info).
- Create custom HealthIndicator (e.g., database check).
- Create custom exception ResourceNotFoundException.
- Throw it from controller/service.
- Create @RestControllerAdvice class.
- Handle ResourceNotFoundException → 404 with custom body.
- Handle MethodArgumentNotValidException → field errors map.
- Handle generic Exception → 500 with message.
- Create ErrorResponse DTO (status, message, timestamp, path).
- Use it in all handlers.
- Test validation error with @Valid + @NotBlank.
- Test 404 endpoint.
- Add logging in exception handlers.
- Mix: Custom exceptions (BusinessException, ValidationException), handle ConstraintViolationException, add request path in error response
- Mix: Custom exceptions (BusinessException, ValidationException), handle ConstraintViolationException, add request path in error response
- Mix: Custom exceptions (BusinessException, ValidationException), handle ConstraintViolationException, add request path in error response
- Mix: Custom exceptions (BusinessException, ValidationException), handle ConstraintViolationException, add request path in error response
- Mix: Custom exceptions (BusinessException, ValidationException), handle ConstraintViolationException, add request path in error response