Skip to content
Chapter 40 · Core Java

Project Lombok in Java: The Complete Guide to Eliminating Boilerplate ​

Developers from other languages often complain that Java needs too much code that has nothing to do with business logic: getters, setters, constructors, toString, equals, hashCode. This repetitive code is called boilerplate.

Lombok is a Java library that reduces boilerplate using annotations. You put an annotation on a class, field, or parameter, and during compilation Lombok injects the code for you. It works with old Java versions (from Java 6) and all later ones, and it is used widely in industry. This chapter covers the ten features you will see most often.


How Lombok Actually Works ​

Lombok is an annotation processor. When your code is compiled, Lombok reads its annotations and adds the generated methods into your class, so the .class file contains them as if you had written them yourself. Your .java file stays short.

Setting Up Lombok ​

You need to do two things:

1. Add the dependency to your pom.xml:

xml
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.46</version>
    <scope>provided</scope>
</dependency>

Use the latest Lombok version. The provided scope means Lombok is needed only at compile time; the generated code is already in the bytecode.

2. Install the IDE plugin and enable annotation processing:

This step is optional but helpful. Your IDE does not know what Lombok is going to generate. Without the plugin, IntelliJ will show red error lines under code that calls generated methods, even though the code compiles and runs perfectly. Installing the Lombok plugin and enabling annotation processing in your IDE settings (Build, Execution, Deployment > Compiler > Annotation Processors > Enable annotation processing) tells your IDE to honor what Lombok is about to do, so you get proper code completion and no false errors.


Feature 1: val and var for Local Variable Type Inference ​

val is used in place of the variable type in a local variable declaration. Lombok infers the type from the initialization expression and makes the variable final (immutable).

var works the same way but does not make the variable final, so you can reassign it later. (Since Java 10, var is built into the language, so no Lombok import is needed for it.)

java
import lombok.val;

public class TypeInferenceExample {
    public void demonstrate() {
        val name = "Alice";    // inferred as final String
        // name = "Bob";       // compile error: val makes it final

        var count = 42;        // inferred as int, not final
        count = count + 1;     // allowed with var

        val numbers = new java.util.ArrayList<String>();
        numbers.add("hello");  // methods work, type is known
    }
}

Important limitation: val and var only work for local variables inside a block. You cannot use them for class fields, method parameters, or return types. The type must always be inferrable from the right hand side initializer expression.


Feature 2: @NonNull for Null Safety ​

@NonNull tells Lombok to generate a null check at the top of a method or constructor for that parameter. If a null value is passed, Lombok throws a NullPointerException with a descriptive message before any other code in the method runs.

java
import lombok.NonNull;

public class UserService {
    public void createUser(@NonNull String username) {
        System.out.println("Creating user: " + username);
    }
}

After compilation, the .class file looks like this:

java
public void createUser(String username) {
    if (username == null) {
        throw new NullPointerException("username is marked non-null but is null");
    }
    System.out.println("Creating user: " + username);
}

Key point: use @NonNull on a method or constructor parameter. You can also put it on a field: then the constructors and setters that Lombok generates for that field include the null check (see @RequiredArgsConstructor below).


Feature 3: @Getter and @Setter ​

This is the most frequently seen Lombok annotation in any real codebase. Instead of writing getter and setter methods manually, you annotate your fields.

java
import lombok.Getter;
import lombok.Setter;

public class Employee {
    @Getter @Setter
    private String name;

    @Getter @Setter
    private int age;

    @Getter
    private final String department; // no setter because it is final
}

For String and numeric fields, the getter is named getName() or getAge(). For boolean fields, the getter is named isActive() following the JavaBeans convention.

Generated methods are public by default. You can change the access level:

java
@Getter(AccessLevel.PRIVATE)
@Setter(AccessLevel.PROTECTED)
private String sensitiveData;

Class Level Annotations ​

Instead of annotating every field, you can annotate the class:

java
@Getter
@Setter
public class Employee {
    private String name;
    private int age;
    private static String companyName; // static field: no getter or setter generated
    private final String id;           // final field: getter generated, NO setter
}

Rules at class level:

  • @Getter applies to all nonstatic fields
  • @Setter applies to all nonstatic and nonfinal fields
  • A final field cannot have a setter because you cannot change a final field after assignment

Overriding at Field Level ​

If you use @Getter/@Setter at class level but want to skip one method for a specific field, use AccessLevel.NONE on that field:

java
@Getter
@Setter
public class Employee {
    private String name;  // gets both getter and setter

    @Setter(AccessLevel.NONE)
    private String id;    // only gets getter, no setter
}

Feature 4: @ToString ​

@ToString generates a toString() method that prints the class name followed by all field names and their values. This is the method you use constantly for logging and debugging.

java
import lombok.ToString;

@ToString
public class Product {
    private String name;
    private double price;
    private boolean inStock;
}
// Output: Product(name=Widget, price=9.99, inStock=true)

Customizing @ToString ​

Excluding fields you do not want logged (useful for sensitive data like passwords):

java
@ToString(exclude = "password")
public class User {
    private String username;
    private String password; // will not appear in toString output
    private String email;
}

Removing field names to reduce log verbosity:

java
@ToString(includeFieldNames = false)
public class Point {
    private int x;
    private int y;
}
// Output: Point(10, 20) instead of Point(x=10, y=20)

Explicitly selecting which fields to include:

java
@ToString(onlyExplicitlyIncluded = true)
public class LargeObject {
    @ToString.Include
    private String id;

    @ToString.Include
    private String name;

    private byte[] rawData; // excluded because onlyExplicitlyIncluded is true
}

Feature 5: @NoArgsConstructor, @AllArgsConstructor, and @RequiredArgsConstructor ​

These three annotations handle constructor generation. The names describe exactly what each one does.

java
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.NonNull;

@NoArgsConstructor        // Employee()
@AllArgsConstructor       // Employee(String name, int age, String department)
@RequiredArgsConstructor  // Employee(String department) - only final and @NonNull fields
public class Employee {
    private String name;
    private int age;
    @NonNull private String department;
}

Understanding @RequiredArgsConstructor ​

This annotation generates a constructor with only final fields and @NonNull annotated fields. This is where @NonNull on fields becomes meaningful. When @NonNull is on a field, @RequiredArgsConstructor includes that field in the constructor parameter list and adds a null check for it.

java
// Generated constructor looks like:
public Employee(@NonNull String department) {
    if (department == null) {
        throw new NullPointerException("department is marked non-null but is null");
    }
    this.department = department;
}

Feature 6: @EqualsAndHashCode ​

This generates both equals() and hashCode() methods that follow the contract between them: objects that are equal must have the same hash code. By default, Lombok uses all nonstatic and nontransient fields.

java
import lombok.EqualsAndHashCode;

@EqualsAndHashCode(exclude = "lastModified")
public class Document {
    private String id;
    private String title;
    private String content;
    private java.time.Instant lastModified; // excluded from equality
}

This is critical for collections like HashMap and HashSet to work correctly. Two Document objects with the same id, title, and content will be considered equal and will produce the same hash code, regardless of their lastModified timestamp.

The contract: if two objects are equal according to equals(), their hashCode() must return the same value. The reverse is not required: two objects can have the same hash code without being equal (that is a hash collision, not a violation). Lombok's generated code always respects this contract.


Feature 7: @Data ​

@Data is a convenience shortcut that combines five annotations into one. When you put @Data on a class, it is equivalent to applying:

  • @ToString
  • @EqualsAndHashCode
  • @Getter on all fields
  • @Setter on all nonfinal fields
  • @RequiredArgsConstructor
java
import lombok.Data;
import lombok.NonNull;

@Data
public class Customer {
    private String name;
    private final String customerId;  // no setter because final
    @NonNull private String email;    // included in required constructor, null checked
}

When compiled, that six line class becomes equivalent to roughly sixty lines of handwritten Java with all methods properly implemented. This is the annotation you will see most often in data transfer objects, entity classes, and service models.

When to use @Data: Use it for plain data carrier classes where you want full mutability with the standard set of methods. If you need immutability, use @Value instead.


Feature 8: @Value ​

@Value is the immutable counterpart of @Data. Where @Data generates a mutable class, @Value generates an immutable one. Here is what @Value does:

  • Makes all fields private and final
  • Makes the class itself final (it cannot be subclassed)
  • Generates getters for all fields (but no setters, since all fields are final)
  • Generates toString, equals, and hashCode
  • Generates an all arguments constructor (which is equivalent to a required args constructor when all fields are final)
java
import lombok.Value;

@Value
public class Money {
    String currency;
    double amount;
}

After compilation this becomes effectively:

java
public final class Money {
    private final String currency;
    private final double amount;

    public Money(String currency, double amount) {
        this.currency = currency;
        this.amount = amount;
    }

    public String getCurrency() { return currency; }
    public double getAmount() { return amount; }

    @Override public String toString() { ... }
    @Override public boolean equals(Object o) { ... }
    @Override public int hashCode() { ... }
}

Feature 9: @Builder ​

The builder pattern is used for two things: creating objects part by part (especially when a constructor would have many parameters), and creating immutable objects in a readable way. Lombok generates the entire builder infrastructure with a single annotation.

java
import lombok.Builder;

@Builder
public class ServerConfig {
    private String host;
    private int port;
    private int maxConnections;
    private boolean useSsl;
    private String sslCertPath;
}

Usage after compilation:

java
ServerConfig config = ServerConfig.builder()
    .host("api.example.com")
    .port(443)
    .maxConnections(100)
    .useSsl(true)
    .sslCertPath("/etc/certs/server.crt")
    .build();

Each setter style method on the builder returns the builder itself, so you can chain them. The build() method is what actually creates the final object.

Notice that ServerConfig has no setter methods. You build the object through the builder, and once built, its state cannot change. This is exactly how builder pattern achieves immutability.

Under the hood: Lombok generates an inner static class called ServerConfigBuilder with a field for each field of the outer class and methods that set each field and return this. The build() method calls a package-private all-arguments constructor that Lombok also generates.


Feature 10: @Cleanup ​

@Cleanup ensures that a resource is automatically closed when execution leaves the current scope. This is Lombok's way of generating try finally blocks for you.

java
import lombok.Cleanup;
import java.io.FileInputStream;
import java.io.IOException;

public class ResourceDemo {
    public void readFile(String path) throws IOException {
        @Cleanup FileInputStream in = new FileInputStream(path);
        // read data from in
        // no need to call in.close() manually
    }
}

After compilation, Lombok generates:

java
public void readFile(String path) throws IOException {
    FileInputStream in = new FileInputStream(path);
    try {
        // read data from in
    } finally {
        if (in != null) {
            in.close();
        }
    }
}

This prevents resource leaks. Even if an exception is thrown while reading the file, the finally block guarantees the stream gets closed.

Plain Java can do the same with try-with-resources (Java 7+).


Check yourself ​

  1. When does Lombok add its generated code: at compile time or at runtime?
  2. What is the difference between Lombok val and var?
  3. With class-level @Getter and @Setter, which fields get no getter, and which get no setter?
  4. How do you stop Lombok from generating the setter for one field when @Setter is on the class?
  5. Which fields does @RequiredArgsConstructor put in the constructor?
  6. @Data is a shortcut for which annotations?
  7. Why does @Value end up with an all-arguments constructor?
  8. What does @Cleanup generate?
Answers
  1. At compile time. The IDE plugin plus annotation processing only help the IDE understand it.
  2. val makes the local variable final; var does not. Both work only for local variables.
  3. Static fields get neither. Final fields get a getter but no setter.
  4. @Setter(AccessLevel.NONE) on that field.
  5. final fields and fields marked @NonNull (with a null check for the latter).
  6. @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on non-final fields, and @RequiredArgsConstructor.
  7. @Value makes every field private final, so "required" fields means all fields.
  8. A try/finally that calls close() on the resource when the scope ends.