Lombok Builder Annotation Pitfall: Why Default Field Values Disappear

A stable public service interface, previously maintained and later handed over, started throwing NullPointerException errors when a new consumer system integrated with it. The error occurred within a conditional block:

if (req.getFlag() 
    && req.getName() != null 
    && req.getType() != null) {
    // execute business logic
}

The req object is the request payload, defined as:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestPayload {
    private Boolean flag = true;
    private String name;
    private String type;
}

Since flag has a default value of true, the initial assumption was that req itself was null. However, logs at the method entry confirmed the object was not null.

The consumer built the object using the Lombok builder pattern:

RequestPayload req = RequestPayload.builder()
    .name("sample") 
    .type("test")
    .build();

Eventhough flag was not explicitly set, the expectation was that it would default to true. Instead, it was null.

Testing with a simple demo confirms the issue:

RequestPayload p1 = new RequestPayload();
System.out.println(p1.getFlag()); // true

RequestPayload p2 = RequestPayload.builder().build();
System.out.println(p2.getFlag()); // null

The @Builder annotation does not automatically apply field initializers. To preserve default values, the @Builder.Default annotation must be used:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestPayload {
    @Builder.Default
    private Boolean flag = true;
    private String name;
    private String type;
}

With this change, both object creation methods retain the default value:

RequestPayload p1 = new RequestPayload();
System.out.println(p1.getFlag()); // true

RequestPayload p2 = RequestPayload.builder().build();
System.out.println(p2.getFlag()); // true

Decompiling the class files reveals the difference. Without @Builder.Default, the builder class initializes flag as null. With the annotation, the builder explicitly assigns the default value defined in the field declaration.

This behavior stems from how Lombok handles field initialization. The library does not automatically inspect all fields for default values due to performance and complexity concerns. Instead, it requires an explicit marker (@Builder.Default) to identify which fields should carry their defaults into the builder.

Historical context shows this was not always the case. In older Lombok versions (e.g., 1.16.16), combining @Builder, @Builder.Default, and @NoArgsConstructor caused the default value to be lost when using the no-args constructor. This was due to an optimization that attempted to prevent double initialization but resulted in surprising behavior.

The issue was reported in 2017 and eventually addressed in version 1.18.2 (released in 2018), though it was classified as a feature enhancement rather than a bugfix in the changelog. Modern Lombok versions (1.18.x+) have resolved this conflict, allowing both constructors and builders to coexist safely with default values.

How Lombok Works: Compile-Time Annotation Processing

Lombok operates during the Java compilation phase using compile-time annotations. It hooks into the javac compiler via the Java SPI (Service Provider Interface) mechanism.

Key implementation details:

  1. Entry Point: The AnnotationProcessor class extends AbstractProcessor from javax.annotation.processing. This is loaded during the compiler's initialization phase.
  2. Class Loading: Lombok uses a custom ShadowClassLoader to load its internal classes, which are often stored with a .SCL.lombok suffix to avoid polluting the project classpath.
  3. Logging Configuration: For log annotations (like @Slf4j), Lombok references predefined templates in classes like LoggingFramework, which contain the exact code snippets to generate for each logging implementation.

The compilation process, as described in Deep Dive into Java Virtual Machine (3rd Edition), includes these phases:

  • Initialization of annotation processors.
  • Parsing and symbol table filling (lexical/syntax analysis).
  • Annotation processing (Lombok's active phase).
  • Semantic analysis and bytecode generation.

Lombok modifies the abstract syntax tree (AST) during the annotation processing phase, injecting methods (getters, setters, builders) before the final bytecode is produced. This is why Lombok features are invisible in source code but present in compiled classes.

For a deeper technical exploration, references such as Deep Dive into JVM Bytecode provide practical examples of writing custom annotation processors similar to Lombok's internals.

Tags: Lombok java Builder Pattern Annotation Processing Default Values

Posted on Thu, 07 May 2026 07:18:21 +0000 by pnoeric