Builder Pattern with Lombok in Java

Overview

The Builder pattern is a creational design pattern that provides a flexible solution for constructing complex objects. Lombok's @Builder annotation automatically generates builder classes, reducing boilerplate code significantly.

Basic Usage

1. Simple Builder with Lombok

import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class User {
private final Long id;
private final String username;
private final String email;
private final String firstName;
private final String lastName;
private final Integer age;
private final Boolean active;
}
// Usage
public class BuilderExample {
public static void main(String[] args) {
User user = User.builder()
.id(1L)
.username("john_doe")
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.age(30)
.active(true)
.build();
System.out.println(user);
// Output: User(id=1, username=john_doe, [email protected], firstName=John, lastName=Doe, age=30, active=true)
}
}

2. Customizing the Builder

import lombok.Builder;
import lombok.ToString;
import java.time.LocalDateTime;
@Builder
@ToString
public class Product {
private final String id;
private final String name;
private final String description;
private final Double price;
private final Integer quantity;
private final LocalDateTime createdAt;
// Custom method in builder
public static class ProductBuilder {
public ProductBuilder createdAt() {
this.createdAt = LocalDateTime.now();
return this;
}
public ProductBuilder priceWithTax(Double taxRate) {
this.price = this.price * (1 + taxRate);
return this;
}
}
}
// Usage
public class CustomBuilderExample {
public static void main(String[] args) {
Product product = Product.builder()
.id("PROD-001")
.name("Laptop")
.description("Gaming laptop")
.price(1000.0)
.priceWithTax(0.18) // Custom method
.quantity(10)
.createdAt() // Custom method
.build();
System.out.println(product);
}
}

Advanced Builder Patterns

3. Builder with Validation

import lombok.Builder;
import lombok.ToString;
import java.util.regex.Pattern;
@Builder
@ToString
public class ValidatedUser {
private final Long id;
private final String username;
private final String email;
private final Integer age;
// Custom builder with validation
public static class ValidatedUserBuilder {
private static final Pattern EMAIL_PATTERN = 
Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
public ValidatedUser build() {
validate();
return new ValidatedUser(id, username, email, age);
}
private void validate() {
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be null or empty");
}
if (email == null || !EMAIL_PATTERN.matcher(email).matches()) {
throw new IllegalArgumentException("Invalid email format");
}
if (age != null && age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
}
}
// Usage
public class ValidationExample {
public static void main(String[] args) {
try {
ValidatedUser user = ValidatedUser.builder()
.id(1L)
.username("valid_user")
.email("invalid-email") // This will throw exception
.age(25)
.build();
} catch (IllegalArgumentException e) {
System.out.println("Validation failed: " + e.getMessage());
}
}
}

4. Immutable Objects with Builder

import lombok.Builder;
import lombok.Value;
import java.util.Collections;
import java.util.List;
@Value
@Builder
public class ImmutableOrder {
String orderId;
String customerId;
List<OrderItem> items;
OrderStatus status;
Double totalAmount;
// Customize the builder to make collections immutable
public static class ImmutableOrderBuilder {
private List<OrderItem> items = Collections.emptyList();
public ImmutableOrderBuilder items(List<OrderItem> items) {
this.items = Collections.unmodifiableList(items);
return this;
}
}
}
@Value
@Builder
class OrderItem {
String productId;
String productName;
Integer quantity;
Double unitPrice;
}
enum OrderStatus {
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}
// Usage
public class ImmutableExample {
public static void main(String[] args) {
List<OrderItem> items = List.of(
OrderItem.builder()
.productId("P001")
.productName("Laptop")
.quantity(1)
.unitPrice(999.99)
.build(),
OrderItem.builder()
.productId("P002")
.productName("Mouse")
.quantity(2)
.unitPrice(25.50)
.build()
);
ImmutableOrder order = ImmutableOrder.builder()
.orderId("ORD-001")
.customerId("CUST-123")
.items(items)
.status(OrderStatus.PENDING)
.totalAmount(1050.99)
.build();
System.out.println(order);
// order.getItems().add(newItem); // This would throw UnsupportedOperationException
}
}

Complex Object Building

5. Nested Builders

import lombok.Builder;
import lombok.ToString;
import java.util.List;
@Builder
@ToString
public class Company {
private final String name;
private final Address address;
private final List<Department> departments;
private final ContactInfo contactInfo;
@Builder
@ToString
public static class Address {
private final String street;
private final String city;
private final String state;
private final String zipCode;
private final String country;
}
@Builder
@ToString
public static class Department {
private final String name;
private final Employee manager;
private final List<Employee> employees;
}
@Builder
@ToString
public static class Employee {
private final String id;
private final String name;
private final String position;
private final Double salary;
}
@Builder
@ToString
public static class ContactInfo {
private final String phone;
private final String email;
private final String website;
}
}
// Usage
public class NestedBuilderExample {
public static void main(String[] args) {
Company company = Company.builder()
.name("Tech Corp")
.address(Company.Address.builder()
.street("123 Main St")
.city("San Francisco")
.state("CA")
.zipCode("94105")
.country("USA")
.build())
.departments(List.of(
Company.Department.builder()
.name("Engineering")
.manager(Company.Employee.builder()
.id("E001")
.name("Alice Smith")
.position("CTO")
.salary(150000.0)
.build())
.employees(List.of(
Company.Employee.builder()
.id("E002")
.name("Bob Johnson")
.position("Senior Developer")
.salary(120000.0)
.build()
))
.build()
))
.contactInfo(Company.ContactInfo.builder()
.phone("+1-555-0123")
.email("[email protected]")
.website("www.techcorp.com")
.build())
.build();
System.out.println(company);
}
}

6. Builder with Default Values

import lombok.Builder;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Builder
@ToString
public class Configuration {
private final String name;
private final String description;
private final Integer timeout;
private final Integer maxRetries;
private final Boolean enabled;
private final List<String> tags;
private final LocalDateTime created;
// Custom builder with default values
public static class ConfigurationBuilder {
private Integer timeout = 30;
private Integer maxRetries = 3;
private Boolean enabled = true;
private List<String> tags = new ArrayList<>();
private LocalDateTime created = LocalDateTime.now();
public ConfigurationBuilder tag(String tag) {
this.tags.add(tag);
return this;
}
public ConfigurationBuilder tags(List<String> tags) {
this.tags.addAll(tags);
return this;
}
}
}
// Usage
public class DefaultValuesExample {
public static void main(String[] args) {
// Using defaults
Configuration config1 = Configuration.builder()
.name("API Config")
.description("Configuration for API service")
.build();
System.out.println("Default config: " + config1);
// Overriding defaults
Configuration config2 = Configuration.builder()
.name("Database Config")
.description("Database connection settings")
.timeout(60)
.maxRetries(5)
.enabled(false)
.tag("database")
.tag("production")
.build();
System.out.println("Custom config: " + config2);
}
}

Advanced Lombok Builder Features

7. Using @Builder with Inheritance

import lombok.Builder;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
// Base class with SuperBuilder for inheritance
@SuperBuilder
@ToString
public abstract class Vehicle {
protected final String vin;
protected final String make;
protected final String model;
protected final Integer year;
protected final Double price;
}
// Child class
@ToString(callSuper = true)
public class Car extends Vehicle {
private final Integer doors;
private final String fuelType;
private final Boolean isAutomatic;
@Builder
public Car(String vin, String make, String model, Integer year, Double price, 
Integer doors, String fuelType, Boolean isAutomatic) {
super(vin, make, model, year, price);
this.doors = doors;
this.fuelType = fuelType;
this.isAutomatic = isAutomatic;
}
}
// Alternative approach with @SuperBuilder for both classes
@SuperBuilder
@ToString(callSuper = true)
class Truck extends Vehicle {
private final Double payloadCapacity;
private final Integer axles;
private final Boolean hasTrailer;
}
// Usage
public class InheritanceExample {
public static void main(String[] args) {
// Using regular builder with constructor
Car car = Car.builder()
.vin("1HGCM82633A123456")
.make("Honda")
.model("Accord")
.year(2023)
.price(28000.0)
.doors(4)
.fuelType("GASOLINE")
.isAutomatic(true)
.build();
System.out.println("Car: " + car);
// Using SuperBuilder
Truck truck = Truck.builder()
.vin("1NPLX1AXXBN123456")
.make("Ford")
.model("F-150")
.year(2023)
.price(45000.0)
.payloadCapacity(3000.0)
.axles(2)
.hasTrailer(true)
.build();
System.out.println("Truck: " + truck);
}
}

8. Builder with Collections and Maps

import lombok.Builder;
import lombok.Singular;
import lombok.ToString;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Builder
@ToString
public class ShoppingCart {
private final String cartId;
private final String customerId;
@Singular
private final List<CartItem> items;
@Singular
private final Set<String> coupons;
@Singular
private final Map<String, String> metadata;
@Builder
@ToString
public static class CartItem {
private final String productId;
private final String productName;
private final Integer quantity;
private final Double price;
}
}
// Usage
public class CollectionBuilderExample {
public static void main(String[] args) {
ShoppingCart cart = ShoppingCart.builder()
.cartId("CART-001")
.customerId("CUST-123")
.item(ShoppingCart.CartItem.builder()
.productId("P001")
.productName("Laptop")
.quantity(1)
.price(999.99)
.build())
.item(ShoppingCart.CartItem.builder()
.productId("P002")
.productName("Mouse")
.quantity(2)
.price(25.50)
.build())
.coupon("WELCOME10")
.coupon("SUMMER2024")
.metadata("sessionId", "SESS-789")
.metadata("userAgent", "Mozilla/5.0")
.build();
System.out.println(cart);
}
}

Real-World Examples

9. API Request Builder

import lombok.Builder;
import lombok.Singular;
import lombok.ToString;
import java.util.Map;
@Builder
@ToString
public class ApiRequest {
private final String url;
private final HttpMethod method;
private final Map<String, String> headers;
private final String body;
private final Integer timeout;
private final Integer maxRetries;
private final Boolean followRedirects;
public enum HttpMethod {
GET, POST, PUT, DELETE, PATCH
}
// Custom builder methods for common scenarios
public static class ApiRequestBuilder {
public ApiRequestBuilder get(String url) {
this.method = HttpMethod.GET;
this.url = url;
return this;
}
public ApiRequestBuilder post(String url) {
this.method = HttpMethod.POST;
this.url = url;
return this;
}
public ApiRequestBuilder jsonBody(String json) {
this.body = json;
header("Content-Type", "application/json");
return this;
}
public ApiRequestBuilder bearerToken(String token) {
header("Authorization", "Bearer " + token);
return this;
}
public ApiRequestBuilder basicAuth(String username, String password) {
String credentials = java.util.Base64.getEncoder()
.encodeToString((username + ":" + password).getBytes());
header("Authorization", "Basic " + credentials);
return this;
}
}
}
// Usage
public class ApiRequestExample {
public static void main(String[] args) {
ApiRequest request = ApiRequest.builder()
.post("https://api.example.com/users")
.jsonBody("{\"name\": \"John\", \"email\": \"[email protected]\"}")
.bearerToken("abc123")
.timeout(5000)
.maxRetries(3)
.header("User-Agent", "MyApp/1.0")
.header("Accept", "application/json")
.build();
System.out.println(request);
}
}

10. Database Entity Builder

import lombok.Builder;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.UUID;
@Builder
@ToString
public class UserEntity {
private final String id;
private final String username;
private final String email;
private final String passwordHash;
private final UserRole role;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
private final Boolean enabled;
private final Boolean accountNonExpired;
private final Boolean accountNonLocked;
private final Boolean credentialsNonExpired;
public enum UserRole {
ADMIN, USER, MODERATOR, GUEST
}
// Custom builder for entity creation
public static class UserEntityBuilder {
public UserEntityBuilder createNew() {
this.id = UUID.randomUUID().toString();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.enabled = true;
this.accountNonExpired = true;
this.accountNonLocked = true;
this.credentialsNonExpired = true;
this.role = UserRole.USER;
return this;
}
public UserEntityBuilder asAdmin() {
this.role = UserRole.ADMIN;
return this;
}
public UserEntityBuilder withEncodedPassword(String rawPassword) {
// In real application, use proper password encoding
this.passwordHash = "encoded_" + rawPassword;
return this;
}
public UserEntityBuilder markUpdated() {
this.updatedAt = LocalDateTime.now();
return this;
}
}
}
// Usage
public class EntityBuilderExample {
public static void main(String[] args) {
UserEntity user = UserEntity.builder()
.createNew()
.username("jane_doe")
.email("[email protected]")
.withEncodedPassword("securePassword123")
.build();
System.out.println("New user: " + user);
UserEntity admin = UserEntity.builder()
.createNew()
.username("admin_user")
.email("[email protected]")
.withEncodedPassword("adminPass123")
.asAdmin()
.build();
System.out.println("Admin user: " + admin);
}
}

Best Practices and Patterns

11. Builder with Validation and Factory Methods

import lombok.Builder;
import lombok.ToString;
import java.util.regex.Pattern;
@Builder(builderClassName = "InternalBuilder", builderMethodName = "internalBuilder")
@ToString
public class EmailMessage {
private final String from;
private final String to;
private final String subject;
private final String body;
private final Priority priority;
public enum Priority {
HIGH, NORMAL, LOW
}
// Factory methods for common scenarios
public static InternalBuilder highPriority() {
return internalBuilder().priority(Priority.HIGH);
}
public static InternalBuilder notification(String to, String subject) {
return internalBuilder()
.to(to)
.subject(subject)
.priority(Priority.NORMAL);
}
public static InternalBuilder alert(String to, String subject) {
return internalBuilder()
.to(to)
.subject("[ALERT] " + subject)
.priority(Priority.HIGH);
}
// Custom builder with validation
public static class InternalBuilder {
private static final Pattern EMAIL_PATTERN = 
Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
public EmailMessage build() {
validate();
return new EmailMessage(from, to, subject, body, priority);
}
private void validate() {
if (to == null || !EMAIL_PATTERN.matcher(to).matches()) {
throw new IllegalArgumentException("Invalid 'to' email address");
}
if (from == null || !EMAIL_PATTERN.matcher(from).matches()) {
throw new IllegalArgumentException("Invalid 'from' email address");
}
if (subject == null || subject.trim().isEmpty()) {
throw new IllegalArgumentException("Subject cannot be empty");
}
}
}
}
// Usage
public class FactoryMethodExample {
public static void main(String[] args) {
// Using factory methods
EmailMessage notification = EmailMessage.notification(
"[email protected]", 
"Welcome to our service!"
)
.from("[email protected]")
.body("Thank you for joining our service!")
.build();
System.out.println("Notification: " + notification);
EmailMessage alert = EmailMessage.alert(
"[email protected]",
"System overload detected"
)
.from("[email protected]")
.body("CPU usage is above 90% for the last 5 minutes.")
.build();
System.out.println("Alert: " + alert);
}
}

12. Thread-Safe Builder Pattern

import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class ThreadSafeConfig {
private final String databaseUrl;
private final String username;
private final String password;
private final Integer poolSize;
private final Long connectionTimeout;
// Thread-safe builder usage pattern
public static class ThreadSafeConfigBuilder {
public ThreadSafeConfig buildThreadSafe() {
// Perform thread-safe operations or validations
validate();
return build();
}
private void validate() {
if (poolSize != null && poolSize <= 0) {
throw new IllegalArgumentException("Pool size must be positive");
}
}
}
}
// Usage in multi-threaded environment
public class ThreadSafeExample {
public static void main(String[] args) {
ThreadSafeConfig config = ThreadSafeConfig.builder()
.databaseUrl("jdbc:mysql://localhost:3306/mydb")
.username("admin")
.password("secret")
.poolSize(10)
.connectionTimeout(30000L)
.buildThreadSafe(); // Explicit thread-safe build method
System.out.println(config);
}
}

Key Benefits of Lombok Builder

  1. Reduced Boilerplate: Automatic generation of builder code
  2. Immutability: Easy creation of immutable objects
  3. Fluent API: Clean, readable object construction
  4. Flexibility: Customizable through builder class customization
  5. Validation: Easy to add validation logic in builder
  6. Default Values: Convenient default value handling

Common Pitfalls and Solutions

  • Missing required fields: Use validation in builder's build() method
  • Mutable collections: Use @Singular or custom collection handling
  • Inheritance complexity: Use @SuperBuilder for inheritance scenarios
  • Validation timing: Perform validation in build() method, not in setters

The Lombok @Builder annotation significantly simplifies the implementation of the Builder pattern while maintaining flexibility and type safety.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper