Fundamentals 12 min read

Master Java Builder Pattern and Avoid Lombok @Builder & @SuperBuilder Pitfalls

This article explains why telescoping constructors and JavaBean setters are problematic, demonstrates a hand‑written Builder implementation based on Effective Java, shows how Lombok @Builder simplifies code, and warns about two common Lombok pitfalls—default values and inheritance—while offering practical solutions.

Tinker Programmer
Tinker Programmer
Tinker Programmer
Master Java Builder Pattern and Avoid Lombok @Builder & @SuperBuilder Pitfalls

Why the traditional constructors and setters are problematic

When a class has many fields, developers often create multiple overloaded constructors (telescoping constructors) or use a no‑arg constructor with a flood of setXxx() calls. This leads to confusing code, easy parameter mix‑ups, and objects that are mutable and unsafe in multithreaded environments.

Telescoping Constructor anti‑pattern

// Only required fields
public RiskControlRequest(String userId, String deviceId) { ... }

// Adding IP
public RiskControlRequest(String userId, String deviceId, String ip) { ... }

// Adding IP and location (constructor explosion)
public RiskControlRequest(String userId, String deviceId, String ip, String location) { ... }

Calling new RiskControlRequest("123", "abc", "192.168.1.1", null) forces the caller to pass null for optional parameters and makes it easy to swap arguments, which can cause serious runtime bugs.

JavaBean setter hell

RiskControlRequest request = new RiskControlRequest();
request.setUserId("123");
request.setDeviceId("abc");
request.setIp("192.168.1.1");
// ... many more setters

Code is verbose and unattractive.

State inconsistency : the object is in a half‑built state after new, which can be read by other threads or placed into a queue prematurely.

Immutability cannot be guaranteed : fields are not final and can be changed at any time, making the object thread‑unsafe.

Effective Java’s recommended solution

Effective Java advises using the Builder pattern when a constructor has more than four parameters. The modern Java implementation creates a static inner Builder class that validates required fields, sets optional fields via chainable methods, and finally builds an immutable object.

Core hand‑written Builder code

public class RiskControlRequest {
    // 1. All fields are final for immutability
    private final String userId;
    private final String deviceId;
    private final String ip;
    private final String location;

    // 2. Private constructor forces use of Builder and validates required fields
    private RiskControlRequest(Builder builder) {
        if (builder.userId == null || builder.deviceId == null) {
            throw new IllegalArgumentException("userId and deviceId cannot be null!");
        }
        this.userId = builder.userId;
        this.deviceId = builder.deviceId;
        this.ip = builder.ip;
        this.location = builder.location;
    }

    // 3. Static inner Builder
    public static class Builder {
        // required fields
        private final String userId;
        private final String deviceId;
        // optional fields
        private String ip;
        private String location;

        public Builder(String userId, String deviceId) {
            this.userId = userId;
            this.deviceId = deviceId;
        }

        public Builder ip(String ip) { this.ip = ip; return this; }
        public Builder location(String location) { this.location = location; return this; }
        public RiskControlRequest build() { return new RiskControlRequest(this); }
    }
}

Elegant client usage

// Clear, type‑safe construction
RiskControlRequest request = new RiskControlRequest.Builder("user_1001", "device_iPhone14")
    .ip("192.168.1.100")
    .location("39.9042,116.4074")
    .build();
// No setters exist, so the object is immutable and thread‑safe.

Using Lombok to generate Builders

Lombok’s @Builder annotation can generate the same boilerplate automatically.

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class UserQueryCondition {
    private String username;
    private Integer age;
    private String status;
}

UserQueryCondition condition = UserQueryCondition.builder()
    .username("张三")
    .age(18)
    .build();

Lombok pitfalls (high‑frequency interview traps)

Pitfall 1: Default values are lost . When a field has an initializer, Lombok’s plain @Builder does not copy that value; you must annotate the field with @Builder.Default to preserve it.

@Builder
public class Config {
    // Must add @Builder.Default, otherwise the default is ignored
    @Builder.Default
    private int timeout = 3000;
}

Pitfall 2: Inheritance conflict . A subclass that also uses @Builder cannot set fields defined in the superclass, leading to compilation errors.

// Incorrect – subclass cannot access parent fields
@Builder
public class BaseEvent { private String traceId; }

@Builder
public class OrderEvent extends BaseEvent { private String orderId; }
// OrderEvent.builder().traceId("123") // compile error

The correct solution is to use Lombok’s @SuperBuilder, which supports inheritance.

@SuperBuilder
public class BaseEvent { private String traceId; }

@SuperBuilder
public class OrderEvent extends BaseEvent { private String orderId; }

OrderEvent event = OrderEvent.builder()
    .traceId("trace-999")
    .orderId("order-111")
    .build();

GoF classic vs. modern Java Builder

Classic GoF : Uses a separate Director to control the construction order, suitable for very complex, hierarchical objects (e.g., XML‑to‑DOM parsing).

Modern Java : Eliminates the Director; the Builder’s fluent API directly assembles the object, covering the majority of business‑level use cases.

Conclusion: For typical CRUD and API payload objects, use the modern Builder (or Lombok). Reserve the Director pattern for low‑level framework code that truly needs controlled assembly steps.

When to adopt the Builder pattern

When a class has more than four fields.

When required and optional fields are mixed.

When you need the resulting object to be immutable (e.g., configuration objects, DTOs in multithreaded contexts).

When you want fluent, chainable construction that reads like natural language.

⚠️ Avoid over‑engineering : If a class only has two or three fields, a simple @Data with setters is sufficient; forcing a Builder adds unnecessary code.

FAQ

Q: How does Builder differ from Factory? Factory decides which concrete class to instantiate, while Builder focuses on step‑by‑step assembly of a complex object’s attributes.

Q: Should I use Lombok @Builder or hand‑write a Builder? For everyday business code, Lombok @Builder saves time. If you need complex validation during construction, a hand‑written Builder gives more control.

Q: What’s the difference between @Builder and @SuperBuilder ? @Builder cannot handle inherited fields; @SuperBuilder is designed for inheritance, allowing both parent and child fields to be set fluently.

Q: Does the Builder pattern guarantee thread safety? An immutable object built with final fields is thread‑safe after construction, but the Builder instance itself is not thread‑safe and should not be shared across threads.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

design patternsjavaLombokBuilder PatternImmutable ObjectsEffective JavaSuperBuilder
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up together!

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.