6 minute read

Java REST APIs accumulate boilerplate fast. Every new resource means another controller, service, DTOs, mapper, and error handling. Multiply by twenty resources and you have thousands of lines of nearly identical code.

This guide covers practical tools that each attack a different layer of the problem. Some reduce the code you write. Others eliminate entire categories of code.

The Layers of Boilerplate

Before choosing tools, understand where the repetition comes from:

Layer What you write Why it repeats
Model Getters, setters, constructors, equals, hashCode Every POJO needs them
Mapping Entity-to-DTO conversion logic Every resource needs a mapper
Controller @GetMapping, @PostMapping, request/response handling Every resource needs CRUD endpoints
Response format Envelope wrappers, pagination, links Every endpoint needs consistent structure
Error handling Exception mappers, validation formatting Duplicated unless centralized

Each layer has its own solution. The most effective strategy combines tools across multiple layers.

Layer 1: Eliminate Model Boilerplate with Lombok

Lombok removes the most tedious code in Java: getters, setters, constructors, builders, equals/hashCode, and toString.

Without Lombok (35 lines):

public class ProductDto {
    private String id;
    private String name;
    private BigDecimal price;

    public ProductDto(String id, String name, BigDecimal price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
    // ... setters, equals, hashCode, toString
}

With Lombok (7 lines):

@Data
@AllArgsConstructor
public class ProductDto {
    private String id;
    private String name;
    private BigDecimal price;
}

Lombok is a compile-time tool — no runtime overhead, no reflection. It works with every framework and IDE. Use @Data for mutable DTOs, @Value for immutable ones, and @Builder for objects with many optional fields.

Impact: Cuts model code by ~70%, but does not reduce the number of classes you need.

Layer 2: Automate Mapping with MapStruct

MapStruct generates type-safe mapper implementations at compile time. Instead of writing mapping logic by hand for every entity-DTO pair, you define an interface.

@Mapper(componentModel = "spring")
public interface ProductMapper {
    ProductDto toDto(ProductEntity entity);
    ProductEntity toEntity(CreateProductRequest request);
}

MapStruct generates the implementation at compile time. It handles field name matching, type conversions, and nested objects. For custom mappings, add @Mapping annotations:

@Mapping(source = "createdAt", target = "creationDate")
@Mapping(target = "fullName", expression = "java(entity.getFirst() + \" \" + entity.getLast())")
ProductDto toDto(ProductEntity entity);

Impact: Eliminates manual mapping code, but you still need a mapper interface per entity-DTO pair.

Layer 3: Reduce Controller Code

Option A: Centralize with @ControllerAdvice

At minimum, centralize error handling so it is not duplicated across controllers:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }
}

This removes error handling from individual controllers, but you still write a controller for every resource. For a deeper look at error handling strategies, see Java REST API Error Handling: Best Practices.

Option B: Generate Controllers from OpenAPI

If your API design starts with an OpenAPI specification, OpenAPI Generator can generate controller interfaces and DTOs from the spec:

openapi-generator generate -i api.yaml -g spring -o ./generated

This produces controller interfaces, model classes, and Spring Boot configuration. You implement the interface methods with your business logic.

Impact: Removes hand-written controller and DTO boilerplate, but requires maintaining an OpenAPI spec as the source of truth. Works well for contract-first teams.

Option C: Eliminate Controllers Entirely

Declarative frameworks remove the controller layer altogether. Instead of generating controllers from a spec, you define resources and let the framework handle routing.

With JsonApi4j, a resource with full CRUD support requires two classes:

@JsonApiResource(resourceType = "projects")
public class ProjectResource implements Resource<ProjectDto> {

    @Override
    public String resolveResourceId(ProjectDto dto) {
        return dto.getId();
    }

    @Override
    public ProjectAttributes resolveAttributes(ProjectDto dto) {
        return new ProjectAttributes(dto.getName(), dto.getStatus());
    }
}
public class ProjectOperations implements ResourceOperations<ProjectDto> {

    @Override
    public ProjectDto readById(JsonApiRequest request) {
        return projectService.findById(request.getResourceId());
    }

    @Override
    public PaginationAwareResponse<ProjectDto> readPage(JsonApiRequest request) {
        return PaginationAwareResponse.cursorAware(
            projectService.findPage(request.getPaginationRequest()),
            request.getPaginationRequest().getCursor()
        );
    }

    @Override
    public ProjectDto create(JsonApiRequest request) {
        return projectService.create(request.getSingleResourceDocPayload());
    }

    @Override
    public void update(JsonApiRequest request) {
        projectService.update(request.getResourceId(), request.getSingleResourceDocPayload());
    }

    @Override
    public void delete(JsonApiRequest request) {
        projectService.delete(request.getResourceId());
    }
}

No controller. No DTOs for request/response. No mapper. The framework generates GET /projects, GET /projects/{id}, POST /projects, PATCH /projects/{id}, and DELETE /projects/{id} — all returning JSON:API-compliant responses with pagination, links, and standardized error formatting.

Impact: Eliminates the controller, DTO, and mapper layers entirely. The resolveAttributes method replaces your mapper, and the JSON:API structure replaces your response envelope.

Layer 4: Standardize Response Format

Without a standard, every developer invents their own envelope:

// Developer A                    // Developer B
{ "data": { ... } }              { "result": { ... } }
{ "error": "not found" }         { "errors": [{ "msg": "..." }] }

JsonApi4j enforces the JSON:API specification across all endpoints automatically. Every response includes data with type, id, and attributes, links for pagination, and consistent errors documents. Features like Sparse Fieldsets and Compound Documents work out of the box.

For APIs that do not need JSON:API, you can still standardize by creating a shared ApiResponse<T> wrapper and enforcing it through code review — but you are maintaining that envelope code yourself.

Combining Tools

These tools are not mutually exclusive. A practical stack might look like:

If your API is… Recommended stack
Simple CRUD over JPA Lombok + Spring Data REST
Contract-first with OpenAPI Lombok + MapStruct + OpenAPI Generator
JSON:API compliant, mixed data sources Lombok + JsonApi4j
Large API with strict consistency Lombok + JsonApi4j + plugins (access control, OpenAPI, sparse fieldsets)

The common thread is Lombok — there is no reason not to use it in a Java project. Beyond that, the choice depends on whether you want to eliminate mapping (MapStruct), controllers (declarative frameworks), or both.

Conclusion

Boilerplate in Java REST APIs is not one problem — it is several. Model ceremony, mapping logic, controller repetition, and inconsistent response formats each have their own solution.

In this guide, you learned:

  • Lombok eliminates POJO ceremony (getters, setters, constructors)
  • MapStruct generates type-safe mappers at compile time
  • OpenAPI Generator creates controllers from specs for contract-first teams
  • Declarative frameworks like JsonApi4j eliminate controllers, mappers, and response formatting entirely
  • Combining tools across layers gives the biggest reduction

Start with the layer that hurts the most in your project, and work outward.


FAQ

What is the biggest source of boilerplate in Java REST APIs?

Controller code and DTO mapping. Every resource needs CRUD endpoints, request/response DTOs, and conversion logic. Tools like JsonApi4j eliminate all three by generating endpoints from resource definitions, while MapStruct automates mapping for traditional controller-based APIs.

Should I use Lombok in my Java project?

Yes. Lombok eliminates getters, setters, constructors, and builder patterns with zero runtime overhead. It is a compile-time annotation processor supported by all major IDEs and build tools. There is almost no downside.

How does MapStruct compare to manual DTO mapping?

MapStruct generates type-safe mapping implementations at compile time, catching mapping errors during build rather than at runtime. It handles field matching, type conversions, and nested objects automatically. Manual mapping is error-prone and tedious at scale.

When should I use OpenAPI Generator vs a declarative framework?

Use OpenAPI Generator if your team follows a contract-first workflow where the API spec is designed before implementation. Use a declarative framework like JsonApi4j if you prefer code-first development and want the framework to handle routing, serialization, and response formatting from your domain model.

Can I combine multiple boilerplate reduction tools?

Absolutely. Lombok works alongside any framework. MapStruct pairs well with controller-based APIs. JsonApi4j replaces controllers and mappers entirely but benefits from Lombok on your DTOs and attribute classes.

Categories:

Updated: