In real systems, APIs don’t fail gracefully by default.
Without discipline, you end up with:
- Different response formats per endpoint
- Unclear error messages
- Controllers full of try-catch blocks
This article explains how we design predictable, consistent API responses using a common response wrapper and centralized exception handling.
This is Part 3 of the Production-Grade Spring Boot API Design series.
Code Gihub Repo: https://github.com/Pratik280/order-system/tree/develop
What this post covers
- Why API response consistency matters in microservices
- Designing a generic BaseResponse
- Builder pattern in API responses
- ResponseEntity and real HTTP semantics
- Global exception handling with @RestControllerAdvice
- Translating domain exceptions into HTTP responses
Why we need a common response format
In microservices:
- APIs must look identical
- Errors must be predictable
- Clients must not guess formats
BaseResponse — A Single Contract for All APIs
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BaseResponse<T> {
private int status;
private String message;
// success payload
private T data;
// error details
private List<Map<String, Object>> errors;
}
Why this structure works
- Generic () — supports any response payload
- Clear separation:
- data → success responses
- errors → failure responses
- Consistent fields across all endpoints
- Easy for clients to consume and validate
Success vs Error clarity
- Success response → data is filled, errors is null
- Error response → errors is filled, data is null
This eliminates ambiguity for API consumers.
Builder Pattern for Clean Response Creation
Instead of manually constructing responses everywhere, we centralize response creation.
ResponseBuilder.java
public class ResponseBuilder {
public static <T> ResponseEntity<BaseResponse<T>> success(
HttpStatus status, String message, T data ){
BaseResponse<T> baseResponse = new BaseResponse<>(
status.value(),
message,
data,
null
);
return ResponseEntity.status(status).body(baseResponse);
}
public static <T> ResponseEntity<BaseResponse<T>> error(
HttpStatus status, String message, List<Map<String, Object>> errors ){
return ResponseEntity.status(status)
.body(BaseResponse.<T>builder()
.status(status.value())
.message(message)
.errors(errors)
.build());
}
}
Why a ResponseBuilder?
- Prevents duplication across controllers
- Enforces one response format everywhere
- Reduces mistakes in status codes and payloads
- Makes controllers thin and readable
Controllers should orchestrate requests, not construct response objects.
Why Controllers Should NOT Handle Exceptions
A common beginner mistake is this:
try {
// business logic
} catch (Exception e) {
// return error response
}
In production, this leads to:
- Repeated try-catch blocks
- Inconsistent error responses
- Bloated controllers
- Hard-to-maintain code
Global Exception Handling with @RestControllerAdvice
GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
What @RestControllerAdvice does
- Applies to all controllers
- Automatically returns JSON responses
- Centralizes exception-to-response translation
It is the REST equivalent of a global error boundary.
Handling Domain Exceptions with @ExceptionHandler
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<BaseResponse<Object>> handleResourceNotfound(
ResourceNotFoundException exception,
HttpServletRequest request ){
return ResponseBuilder.error(
HttpStatus.NOT_FOUND,
exception.getMessage(),
List.of(Map.of(
"code", "RESOURCENOTFOUND",
"message", exception.getMessage(),
"path", request.getRequestURI()
))
);
}
Why this is production-grade
- Services throw domain-specific exceptions
- Controllers remain clean
- HTTP status codes are correct
- Error payloads are structured and informative
Each error includes:
- Error code (for frontend / monitoring)
- Human-readable message
- Request path (debugging)
Custom Exceptions Represent Business Failures
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message){
super(message);
}
}
Why custom exceptions matter
- Express business meaning, not technical failure
- Decouple service logic from HTTP concerns
- Make error handling explicit and readable
Services should throw what went wrong, not how to respond.
The End Result
With this approach:
- Every API response looks the same
- Error handling is centralized
- Controllers stay lean
- APIs are predictable and client-friendly
- The system scales across teams
This is the difference between APIs that merely work and APIs that survive production.
Final Thoughts
This is the exact structure we followed in real Spring Boot microservices:
- Clean layers
- Explicit dependencies
- Consistent APIs
- Interview‑defensible design
Production‑grade Spring Boot is less about annotations — and more about discipline and structure.
This concludes the Production-Grade Spring Boot API Design series — based on real project experience and refined personal notes.
The goal was simple:
APIs that are clean, consistent, scalable, and interview-ready.
📌 This blog is based on real project experience and refined personal notes.
Top comments (1)
Great content, helped a lot to understand exception handling. If you're interested in spring boot related content, follow me for such content. I upload daily on fundamental topics of Spring Boot, you might like that..... Happy learning