As Java applications grow, creating and initializing objects can become a cumbersome task. We’ve all seen them: constructors with a long list of parameters, some of which are optional, leading to multiple overloaded constructors or the error-prone “telescoping constructor” anti-pattern. The traditional solution is the Builder design pattern, which provides a readable, fluent API for object creation. However, writing a Builder by hand for every DTO or entity is tedious and adds significant boilerplate code to your project.
This is where Project Lombok’s @Builder annotation comes to the rescue. It automatically generates the complete, robust Builder pattern implementation for your class at compile time, leaving your source code clean, concise, and focused on its primary responsibility.
In this guide, we’ll dive deep into @Builder, from basic setup to its most powerful and advanced features.
1. Why Do We Need a Builder?
Let’s consider a simple Employee class. Without a builder, you might initialize it like this:
// Telescoping constructors - hard to read, easy to make mistakes
Employee emp1 = new Employee(1L, "Ankur", "Kumar", "DEV", "ACTIVE");
Employee emp2 = new Employee(2L, "John", "Doe", "QA"); // What is the default status?
This is hard to read. Which string is the first name? Which is the department? Using the Builder pattern improves this significantly:
// Ideal builder usage - clear and readable
Employee emp1 = Employee.builder()
.id(1L)
.firstName("Ankur")
.lastName("Kumar")
.department("DEV")
.status("ACTIVE")
.build();
This approach is self-documenting, flexible, and promotes immutability. Lombok’s @Builder gives us this elegant API without writing a single line of the builder implementation ourselves.
2. Setting Up Lombok
Before using @Builder, you need to add the Lombok dependency to your project and ensure your IDE is configured to process annotations.
Maven Dependency
Add the following to your pom.xml:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version> <!-- Use the latest version -->
<scope>provided</scope>
</dependency>
</dependencies>
Gradle Dependency
For Gradle projects, add this to your build.gradle:
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.30' // Use the latest version
annotationProcessor 'org.projectlombok:lombok:1.18.30'
}
IDE Setup
You must enable annotation processing in your IDE.
- IntelliJ IDEA: Go to
Settings/Preferences>Build, Execution, Deployment>Compiler>Annotation Processorsand check “Enable annotation processing”. - Eclipse: Download the
lombok.jarand run it as an executable (e.g.,java -jar lombok.jar). This will open an installer that can automatically configure your Eclipse installation.
3. The Basic @Builder in Action
Let’s apply @Builder to our Employee class. To make it a useful immutable DTO, we’ll also add @Value, which bundles @Getter, @ToString, @EqualsAndHashCode, and makes all fields private and final.
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class Employee {
Long id;
String firstName;
String lastName;
String department;
String status;
}
That’s it! With these two annotations, Lombok generates:
- A private all-arguments constructor.
- A static method named
builder()that returns a newEmployeeBuilderinstance. - An inner static class named
EmployeeBuilder. - Fluent setter-like methods in the builder for each field (e.g.,
id(Long id)). - A
build()method in the builder that calls the private constructor and returns the finalEmployeeinstance.
You can now create objects with the clean, fluent API we saw earlier:
Employee employee = Employee.builder()
.id(101L)
.firstName("Sanjay")
.lastName("Gupta")
.department("ENGINEERING")
.status("ACTIVE")
.build();
System.out.println(employee);
// Output: Employee(id=101, firstName=Sanjay, lastName=Gupta, department=ENGINEERING, status=ACTIVE)
4. Advanced Features of @Builder
Lombok’s @Builder offers powerful features for more complex scenarios.
Modifying Objects with toBuilder()
Since our Employee class is immutable, we can’t change its state after creation. But what if you need a copy of an object with just one or two fields changed? The toBuilder = true parameter generates a toBuilder() method on your instance, which creates a new builder pre-populated with the values of that instance.
@Value
@Builder(toBuilder = true)
public class Employee {
// ... same fields as before
}
Now, you can easily create modified copies:
// Create the original employee
Employee leadDev = Employee.builder()
.id(101L)
.firstName("Sanjay")
.department("ENGINEERING")
.build();
// Promote the employee to a new role, keeping all other details the same
Employee manager = leadDev.toBuilder()
.department("MANAGEMENT")
.status("PROMOTED")
.build();
System.out.println(manager);
// Output: Employee(id=101, firstName=Sanjay, lastName=null, department=MANAGEMENT, status=PROMOTED)
Setting Default Values with @Builder.Default
A common pitfall is assuming that initializing a field directly will work with @Builder.
private String status = "PENDING"; // This will NOT work as expected with @Builder
When you use the builder, any fields you don’t explicitly set will be null (or 0/false for primitives), because the builder doesn’t know about the default value you assigned. The correct way to set defaults is with @Builder.Default.
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Task {
private final String name;
@Builder.Default
private final String status = "PENDING";
@Builder.Default
private final long createdAt = System.currentTimeMillis();
}
Now, when you build a Task without specifying these fields, their default values will be applied.
Task task = Task.builder().name("Deploy to production").build();
System.out.println(task.getName()); // Output: Deploy to production
System.out.println(task.getStatus()); // Output: PENDING
Handling Collections with @Singular
Working with collection fields in a builder can be awkward. The @Singular annotation provides a much more ergonomic API for adding items one by one.
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;
import java.util.Set;
@Getter
@Builder
public class User {
private String username;
@Singular
private Set roles;
}
The @Singular annotation generates two methods:
- A “singular” method to add one item to the collection (e.g.,
role(String role)). - A “plural” method to set the entire collection at once (e.g.,
roles(Set roles)).
This allows for a highly readable construction:
User adminUser = User.builder()
.username("admin")
.role("ADMIN")
.role("USER_MANAGER")
.build();
System.out.println(adminUser.getRoles());
// Output: [ADMIN, USER_MANAGER]
Lombok is smart enough to use the correct pluralization and immutable collections (like Guava’s or Java 9+’s) by default.
5. Customizing the Builder
You can also customize the names of the builder methods and class.
builderClassName: Specifies the name of the generated builder class.builderMethodName: Changes the name of the static method that returns the builder (default isbuilder()). An empty string""prevents its creation.buildMethodName: Changes the name of the final build method (default isbuild()).setterPrefix: Prefixes all builder methods with a given string.
@Getter
@Builder(
builderClassName = "ProductCreator",
builderMethodName = "create",
buildMethodName = "construct",
setterPrefix = "with"
)
public class Product {
private final String name;
private final double price;
}
// Usage with custom names
Product product = Product.create()
.withName("Super Widget")
.withPrice(99.99)
.construct();
6. Using @Builder on Constructors or Methods
Sometimes you don’t want the builder to use all fields of a class, or you want to perform some validation logic during object creation. In such cases, you can place the @Builder annotation on a constructor or a static factory method instead of the class itself.
When placed on a constructor, only the parameters of that constructor become part of the builder.
import lombok.Builder;
import lombok.ToString;
@ToString
public class Widget {
private String name;
private int weight;
@Builder
public Widget(String name, int weight) {
if (weight < 0) {
throw new IllegalArgumentException("Weight cannot be negative");
}
this.name = name;
this.weight = weight;
}
}
// This will succeed
Widget w1 = Widget.builder().name("sprocket").weight(10).build();
// This will throw IllegalArgumentException
Widget w2 = Widget.builder().name("cog").weight(-5).build();
This pattern is powerful for enforcing invariants while still enjoying the fluent API of a builder.
Conclusion
Lombok’s @Builder is a game-changer for writing clean, modern Java. It eliminates vast amounts of boilerplate code associated with the Builder pattern, leading to code that is more readable, maintainable, and less prone to errors.
By mastering its basic use and advanced features like @Builder.Default, @Singular, and toBuilder(), you can significantly improve your productivity and the overall quality of your codebase. It is, without a doubt, one of the most essential annotations in the Lombok toolkit and a must-have for any Java developer.