Spring BootAPI DesignJava

API Design Patterns We Use in Every Spring Boot Project

developersEra Team|2026-03-05|9 min read

After building dozens of Spring Boot applications, we have settled on a set of API design patterns that we apply to every project. These are not theoretical best practices from a textbook -- they are patterns that have survived production, scaling, and the test of real users hitting real endpoints.

Uniform Error Response Format

Every API we build returns errors in the same structure, regardless of where the error originates:

{
  "error": "RESOURCE_NOT_FOUND",
  "message": "User with ID 42 not found",
  "status": 404,
  "timestamp": "2026-03-05T10:30:00Z"
}

We achieve this with a global @RestControllerAdvice that catches all exceptions and maps them to this format. Controllers never return error responses directly -- they throw exceptions, and the global handler takes care of the rest.

This means frontend developers always know what an error response looks like. No surprises, no special cases.

Always Paginate List Endpoints

Returning unbounded collections is one of the most common API mistakes. A /users endpoint that returns all 50,000 users in a single response will work fine in development and catastrophically fail in production.

Every list endpoint in our APIs uses Spring Data's Pageable:

@GetMapping("/users")
public Page<UserResponse> getUsers(Pageable pageable) {
    return userService.findAll(pageable);
}

The response includes total count, page size, current page, and navigation metadata. Clients always know how much data exists and how to fetch the next page.

Default page size is 20. Maximum page size is 100. These are enforced globally.

Request Validation at the Boundary

We validate all incoming data at the controller layer -- the system boundary -- and trust everything inside the service layer. This means validation annotations on DTOs, not scattered if checks throughout the codebase.

public record CreateUserRequest(
    @NotBlank @Size(max = 100) String name,
    @NotBlank @Email String email,
    @NotNull UserRole role
) {}

Validation errors are caught by our global exception handler and returned in the standard error format with field-level details.

API Versioning From Day One

We version every API from the start: /api/v1/users. Even if we never create a v2, the versioned prefix costs nothing and saves enormous pain if we ever need to make breaking changes.

The version is in the URL path, not in headers. This makes APIs browsable, testable with curl, and easy to route at the reverse proxy level.

DTOs for Everything

We never expose JPA entities directly in API responses. Every endpoint uses dedicated request and response DTOs. This gives us:

  • Decoupling: Database schema changes do not break the API contract
  • Security: Sensitive fields like passwords or internal IDs are never accidentally exposed
  • Flexibility: Response shapes can differ from database shapes without awkward mapping

Java records make this almost zero-cost in terms of boilerplate:

public record UserResponse(
    Long id,
    String name,
    String email,
    String role,
    LocalDateTime createdAt
) {
    public static UserResponse from(User user) {
        return new UserResponse(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getRole().name(),
            user.getCreatedAt()
        );
    }
}

Proper HTTP Status Codes

We use status codes correctly and consistently:

  • 200 OK -- Successful GET, PUT, PATCH
  • 201 Created -- Successful POST that creates a resource (with Location header)
  • 204 No Content -- Successful DELETE
  • 400 Bad Request -- Validation failures
  • 401 Unauthorized -- Missing or invalid authentication
  • 403 Forbidden -- Authenticated but insufficient permissions
  • 404 Not Found -- Resource does not exist
  • 409 Conflict -- Duplicate resource or state conflict
  • 500 Internal Server Error -- Unexpected server failure

Getting these right is not pedantic. Clients, monitoring tools, and API gateways all rely on status codes to make decisions.

Rate Limiting and Security

Every public-facing API gets rate limiting. We use Bucket4j with Redis for distributed rate limiting that works across multiple application instances.

Authentication uses JWT tokens with short expiration times and refresh token rotation. Every endpoint is denied by default -- access must be explicitly granted per role.

The Result

These patterns create APIs that are predictable, secure, and pleasant to work with. Frontend developers know exactly what to expect. New team members can navigate the codebase quickly because everything follows the same structure. And when something goes wrong in production, the consistent error handling and logging make debugging straightforward.

Consistency is not exciting. But it is the foundation that makes everything else possible.

Need help building something like this?

We build production-grade systems. Let's talk about your project.

Start a Conversation →