Why You Should Stop Using Lombok’s @Builder and What to Use Instead

This article explains the hidden pitfalls of Lombok’s @Builder annotation, demonstrates how it deviates from the classic Builder pattern, and presents safer alternatives such as @Accessors with practical code examples and guidance for mixing required and optional parameters in Java projects.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Why You Should Stop Using Lombok’s @Builder and What to Use Instead

2. Scenario Reproduction

2.1 Without @Builder

Class definition:

package io.gitrebase.demo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class APIResponse<T> {
    private T payload;
    private Status status;
}

Usage example:

package io.gitrebase.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(assignableTypes = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public APIResponse handleException(Exception exception) {
        log.error("Unhandled Exception", exception);
        Status status = new Status();
        status.setResponseCode("RESPONSE_CODE_IDENTIFIER");
        status.setDescription("Bla Bla Bla");
        APIResponse response = new APIResponse();
        response.setStatus(status);
        return response;
    }
}

2.2 With @Builder

Class definition:

package io.gitrebase.demo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class APIResponse<T> {
    private T payload;
    private Status status;
}

Usage example:

package io.gitrebase.demo;

import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public APIResponse handleException(Exception exception) {
        log.error("Unhandled Exception", exception);
        var status = new Status().setResponseCode("RESPONSE_CODE_IDENTIFIER").setDescription("Bla Bla Bla");
        return new APIResponse().setStatus(status);
    }
}

3. Why @Builder Is Not Recommended

@Builder generates a builder that cannot distinguish required from optional parameters, which may lead to incorrect or inconsistent object construction.

When combined with @Data, the generated builder becomes mutable with setter methods, violating the immutability principle of the Builder pattern.

The builder produced by @Builder is tied to a concrete type and cannot adapt to different parameter abstractions, limiting the flexibility that the pattern normally provides.

@Builder is only suitable for objects with many parameters where most are optional; for simple fluent object creation it is not the best choice.

4. Alternatives

4.1 Preferred: @Accessors

Class definition using Lombok’s @Accessors (chain = true):

package io.gitrebase.demo;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class APIResponse<T> {
    private T payload;
    private Status status;
}

Compiled class (relevant parts):

package io.gitrebase.demo;

import lombok.experimental.Accessors;

@Accessors(chain = true)
public class APIResponse<T> {
    private T payload;
    private Status status;

    public T getPayload() { return this.payload; }
    public APIResponse<T> setPayload(T payload) { this.payload = payload; return this; }
    public Status getStatus() { return this.status; }
    public APIResponse<T> setStatus(Status status) { this.status = status; return this; }
}

Usage example:

package io.gitrebase.demo;

import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackageClasses = io.gitrebase.demo.RestApplication.class)
public class ApplicationExceptionHandler {
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public APIResponse handleException(Exception exception) {
        log.error("Unhandled Exception", exception);
        var status = new Status().setResponseCode("RESPONSE_CODE_IDENTIFIER").setDescription("Bla Bla Bla");
        return new APIResponse().setStatus(status);
    }
}

The @Accessors annotation also supports advanced configuration options (fluent, chain, prefix). The full definition is shown below for reference:

/**
 * A container for settings for the generation of getters and setters.
 *
 * Complete documentation is found at the Project Lombok features page for @Accessors.
 */
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
    /**
     * If true, accessors will be named after the field without a get/set prefix.
     * Default: false
     */
    boolean fluent() default false;

    /**
     * If true, setters return this instead of void (enabling chaining).
     * Default: false (unless fluent=true, then true)
     */
    boolean chain() default false;

    /**
     * Prefixes to strip from field names when generating accessors.
     */
    String[] prefix() default {};
}

If a class has required parameters and optional ones, define required fields in a constructor and use @Accessors for the optional fields.

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class Person {
    // required fields
    private String name;
    private int id;
    // optional fields
    private int age;
    private String address;

    public Person(String name, int id) {
        this.name = name;
        this.id = id;
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Zhang San", 1001).setAge(25).setAddress("Beijing");
        System.out.println(person);
    }
}

4.2 Manually Simulating @Accessors

For developers worried about the stability of @Accessors (it resides in lombok.experimental), the same fluent API can be achieved by manually writing setter methods that return this.

5. Takeaways

Most developers use Lombok annotations without inspecting the generated source code; spending time reading the underlying code helps avoid hidden pitfalls.

When using Lombok, you should be able to mentally reconstruct the compiled code; otherwise you will eventually encounter bugs.

Not every widely‑used feature is the best choice—consider whether a feature truly fits your design before adopting it. In many cases, @Accessors provides behavior closer to a proper Builder pattern than @Builder.

References

https://blog.csdn.net/w605283073/article/details/130190814

https://medium.com/gitrebase/oh-stop-using-builder-9061a5911d8c

Source: mingmingruyue.blog.csdn.net/article/details/132417856

JavalombokBuilderAccessors
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.