or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

builder-pattern.mdconstructors.mddata-classes.mdexperimental.mdimmutable-patterns.mdindex.mdlogging.mdobject-methods.mdproperty-access.mdtype-inference.mdutilities.md
tile.json

builder-pattern.mddocs/

Builder Pattern

Comprehensive builder pattern implementation with support for inheritance, defaults, collection handling, and extensive customization options.

Capabilities

@Builder Annotation

Generates a complete builder pattern implementation with fluent method chaining and customizable generation options.

/**
 * The builder annotation creates a so-called 'builder' aspect to the class that is annotated or the class
 * that contains a member which is annotated with @Builder.
 * 
 * If a member is annotated, it must be either a constructor or a method. If a class is annotated,
 * then a package-private constructor is generated with all fields as arguments, and it is as if this
 * constructor has been annotated with @Builder instead.
 */
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
    /**
     * @return Name of the method that creates a new builder instance. Default: "builder". 
     * If the empty string, suppress generating the builder method.
     */
    String builderMethodName() default "builder";
    
    /**
     * @return Name of the method in the builder class that creates an instance of your @Builder-annotated class.
     */
    String buildMethodName() default "build";
    
    /**
     * Name of the builder class.
     * 
     * Default for @Builder on types and constructors: (TypeName)Builder.
     * Default for @Builder on methods: (ReturnTypeName)Builder.
     * 
     * @return Name of the builder class that will be generated.
     */
    String builderClassName() default "";
    
    /**
     * If true, generate an instance method to obtain a builder that is initialized with the values of this instance.
     * Legal only if @Builder is used on a constructor, on the type itself, or on a static method that returns
     * an instance of the declaring type.
     * 
     * @return Whether to generate a toBuilder() method.
     */
    boolean toBuilder() default false;
    
    /**
     * Sets the access level of the generated builder class. By default, generated builder classes are public.
     * Note: This does nothing if you write your own builder class (we won't change its access level).
     * 
     * @return The builder class will be generated with this access modifier.
     */
    AccessLevel access() default AccessLevel.PUBLIC;

    /**
     * Prefix to prepend to 'set' methods in the generated builder class. By default, generated methods do not include a prefix.
     *
     * For example, a method normally generated as someField(String someField) would instead be
     * generated as withSomeField(String someField) if using @Builder(setterPrefix = "with").
     * 
     * @return The prefix to prepend to generated method names.
     */
    String setterPrefix() default "";
}

Usage Examples:

import lombok.Builder;

@Builder
public class User {
    private String name;
    private int age;
    private String email;
    private boolean active;
}

// Generated Builder class and methods:
// public static UserBuilder builder() { return new UserBuilder(); }
// public static class UserBuilder {
//     private String name;
//     private int age;
//     private String email;
//     private boolean active;
//     
//     public UserBuilder name(String name) { this.name = name; return this; }
//     public UserBuilder age(int age) { this.age = age; return this; }
//     public UserBuilder email(String email) { this.email = email; return this; }
//     public UserBuilder active(boolean active) { this.active = active; return this; }
//     public User build() { return new User(name, age, email, active); }
// }

// Usage
User user = User.builder()
    .name("John Doe")
    .age(30)
    .email("john@example.com")
    .active(true)
    .build();

With Custom Configuration:

@Builder(
    builderMethodName = "create",
    buildMethodName = "construct",
    setterPrefix = "with"
)
public class Product {
    private String name;
    private double price;
}

// Usage
Product product = Product.create()
    .withName("Laptop")
    .withPrice(999.99)
    .construct();

@Builder.Default Annotation

Specifies default values for builder fields that are used when the field is not explicitly set.

/**
 * The field annotated with @Default must have an initializing expression; 
 * that expression is taken as the default to be used if not explicitly set during building.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Default {
}

Usage Examples:

import lombok.Builder;

@Builder
public class Configuration {
    private String host;
    
    @Builder.Default
    private int port = 8080;
    
    @Builder.Default
    private boolean ssl = false;
    
    @Builder.Default
    private List<String> allowedHosts = new ArrayList<>();
}

// Usage
Configuration config = Configuration.builder()
    .host("localhost")
    // port will be 8080, ssl will be false, allowedHosts will be empty ArrayList
    .build();

@Singular Annotation

Used with @Builder to generate methods for adding individual items to collections, maps, and arrays.

/**
 * Used with @Builder to generate singular add methods for collections.
 * The generated builder will have methods to add individual items to the collection,
 * as well as methods to add all items from another collection.
 */
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface Singular {
    /**
     * @return The singular name to use for the generated methods. 
     * If not specified, lombok will attempt to singularize the field name.
     */
    String value() default "";
    
    /**
     * @return Whether to ignore null collections when calling the bulk methods.
     */
    boolean ignoreNullCollections() default false;
}

Usage Examples:

import lombok.Builder;
import lombok.Singular;
import java.util.List;
import java.util.Set;
import java.util.Map;

@Builder
public class Team {
    @Singular
    private List<String> members;
    
    @Singular("skill")
    private Set<String> skills;
    
    @Singular
    private Map<String, Integer> scores;
}

// Generated methods for members:
// public TeamBuilder member(String member) { /* add single member */ }
// public TeamBuilder members(Collection<? extends String> members) { /* add all members */ }
// public TeamBuilder clearMembers() { /* clear all members */ }

// Generated methods for skills:  
// public TeamBuilder skill(String skill) { /* add single skill */ }
// public TeamBuilder skills(Collection<? extends String> skills) { /* add all skills */ }
// public TeamBuilder clearSkills() { /* clear all skills */ }

// Generated methods for scores:
// public TeamBuilder score(String key, Integer value) { /* add single score */ }  
// public TeamBuilder scores(Map<? extends String, ? extends Integer> scores) { /* add all scores */ }
// public TeamBuilder clearScores() { /* clear all scores */ }

// Usage
Team team = Team.builder()
    .member("Alice")
    .member("Bob")
    .skill("Java")
    .skill("Python")
    .score("Alice", 95)
    .score("Bob", 87)
    .build();

@Builder.ObtainVia Annotation

Specifies how to obtain values for toBuilder() functionality when the field name or access pattern differs from the default.

/**
 * Put on a field (in case of @Builder on a type) or a parameter (for @Builder on a constructor or static method) to
 * indicate how lombok should obtain a value for this field or parameter given an instance; this is only relevant if toBuilder is true.
 * 
 * You do not need to supply an @ObtainVia annotation unless you wish to change the default behaviour: Use a field with the same name.
 * 
 * Note that one of field or method should be set, or an error is generated.
 */
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface ObtainVia {
    /**
     * @return Tells lombok to obtain a value with the expression this.value.
     */
    String field() default "";
    
    /**
     * @return Tells lombok to obtain a value with the expression this.method().
     */
    String method() default "";
    
    /**
     * @return Tells lombok to obtain a value with the expression SelfType.method(this); requires method to be set.
     */
    boolean isStatic() default false;
}

Usage Examples:

@Builder(toBuilder = true)
public class Person {
    private String firstName;
    private String lastName;
    
    @Builder.ObtainVia(method = "getFullName")
    private String fullName;
    
    @Builder.ObtainVia(field = "internalAge")
    private int age;
    
    private int internalAge;
    
    public String getFullName() {
        return firstName + " " + lastName;
    }
}

// toBuilder() will use getFullName() method and internalAge field
Person person = new Person("John", "Doe", "John Doe", 30);
Person modified = person.toBuilder().age(31).build();

Advanced Builder Patterns

Builder on Methods

Apply @Builder to static factory methods:

public class Range {
    private final int start;
    private final int end;
    
    private Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    
    @Builder(builderMethodName = "range")
    public static Range create(int start, int end) {
        return new Range(start, end);
    }
}

// Usage
Range range = Range.range()
    .start(1)
    .end(10)
    .build();

Builder on Constructors

Apply @Builder to specific constructors:

public class Employee {
    private final String name;
    private final String department;
    private final double salary;
    
    @Builder
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
        this.salary = 0.0;
    }
    
    // Other constructors without @Builder
    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }
}

Builder with Inheritance

@Builder
public class Vehicle {
    private String make;
    private String model;
    private int year;
}

// For inheritance, consider using @SuperBuilder instead

ToBuilder Functionality

Generate builders from existing instances:

@Builder(toBuilder = true)
public class Settings {
    private String theme;
    private boolean darkMode;
    private int fontSize;
}

// Usage
Settings original = Settings.builder()
    .theme("default")
    .darkMode(false)
    .fontSize(12)
    .build();

Settings modified = original.toBuilder()
    .darkMode(true)
    .fontSize(14)
    .build();

Builder Best Practices

Validation in Builder

@Builder
public class User {
    private String email;
    private int age;
    
    // Custom builder class to add validation
    public static class UserBuilder {
        public User build() {
            if (email == null || !email.contains("@")) {
                throw new IllegalArgumentException("Invalid email");
            }
            if (age < 0) {
                throw new IllegalArgumentException("Age cannot be negative");
            }
            return new User(email, age);
        }
    }
}

Complex Collection Handling

@Builder
public class Report {
    @Singular("entry")
    private Map<String, List<String>> entries;
    
    @Singular
    private List<Tag> tags;
}

// Usage with complex collections
Report report = Report.builder()
    .entry("errors", Arrays.asList("Error 1", "Error 2"))
    .entry("warnings", Arrays.asList("Warning 1"))
    .tag(new Tag("important"))
    .tag(new Tag("reviewed"))
    .build();

@Jacksonized Integration

Configures Jackson JSON serialization to work seamlessly with Lombok builders.

/**
 * The @Jacksonized annotation is an add-on annotation for @Builder, @SuperBuilder, and @Accessors.
 * 
 * For @Builder and @SuperBuilder, it automatically configures the generated builder class to be used by Jackson's
 * deserialization. It only has an effect if present at a context where there is also a @Builder or a @SuperBuilder.
 */
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface Jacksonized {
}

Usage Examples:

import lombok.Builder;
import lombok.extern.jackson.Jacksonized;
import com.fasterxml.jackson.databind.ObjectMapper;

@Builder
@Jacksonized
public class ApiResponse {
    private String status;
    private String message;
    private Object data;
}

// Jackson can now deserialize JSON directly to builder
ObjectMapper mapper = new ObjectMapper();
String json = """
    {
        "status": "success",
        "message": "Request processed",
        "data": {"id": 123}
    }
    """;

ApiResponse response = mapper.readValue(json, ApiResponse.class);

With @SuperBuilder:

import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;

@SuperBuilder
@Jacksonized
public class BaseEntity {
    private String id;
    private long timestamp;
}

@SuperBuilder
@Jacksonized  
public class User extends BaseEntity {
    private String name;
    private String email;
}

// Jackson can deserialize to inherited builder classes
String userJson = """
    {
        "id": "user123",
        "timestamp": 1234567890,
        "name": "John Doe", 
        "email": "john@example.com"
    }
    """;

User user = mapper.readValue(userJson, User.class);

What @Jacksonized Does:

  1. Configures Jackson to use the builder for deserialization with @JsonDeserialize(builder=...)
  2. Copies Jackson configuration annotations from the class to the builder class
  3. Inserts @JsonPOJOBuilder(withPrefix="") on the generated builder class
  4. Respects custom setterPrefix and buildMethodName configurations
  5. For @SuperBuilder, makes the builder implementation class package-private

Builder Method Prefix Configuration:

@Builder(setterPrefix = "with")
@Jacksonized
public class ConfiguredBuilder {
    private String name;
    private int value;
}

// Generated builder methods: withName(), withValue()
// Jackson is automatically configured to recognize the "with" prefix