← Home

Production-Grade Spring Boot APIs — Part 2: Consistent Responses & Global Exception Handling

26 January, 2026


In real systems, APIs don’t fail gracefully by default.

Without discipline, you end up with:

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.

Refer previous parts:

Code Gihub Repo: https://github.com/Pratik280/order-system/tree/develop


What this post covers


Why we need a common response format

In microservices:


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

Success vs Error clarity

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?

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:


Global Exception Handling with @RestControllerAdvice

GlobalExceptionHandler.java

@RestControllerAdvice
public class GlobalExceptionHandler {

What @RestControllerAdvice does

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

Each error includes:


Custom Exceptions Represent Business Failures

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message){
        super(message);
    }
}

Why custom exceptions matter

Services should throw what went wrong, not how to respond.


The End Result

With this approach:

This is the difference between APIs that merely work and APIs that survive production.


This is Part 2 of a 3-part series on Production-Grade Spring Boot API Design.

👉 Part 3 focuses on production-Grade Folder Structure & layered Implementation.