Skip to content

Project Lombok in Java: The Complete Guide to Eliminating Boilerplate

If you have ever shown a Java class to a developer who works primarily in Python or Go, you already know the reaction. Their eyes glaze over when they see twenty lines of getters, twenty lines of setters, a constructor, a toString, an equals, a hashCode, and maybe a builder sitting on top of what is essentially a four field data class. They say something like "why do you write so much unnecessary code?" That complaint is completely fair, and Project Lombok is the Java community's answer to it.

Lombok is a Java library with one job: reduce boilerplate code using annotations. You put an annotation on a field, a class, or a method parameter, and Lombok generates the repetitive code for you during compilation. Your source file stays short and readable. The compiled bytecode contains every method a consumer of your class would expect. It is the best of both worlds.

This guide covers every major Lombok feature you will encounter in production code, how Lombok actually works under the hood, and all the interview questions you need to be ready for.


Why Boilerplate Is a Real Problem

Before you learn Lombok, it helps to feel the pain it solves.

Imagine a simple Employee class with four fields: name, age, department, and salary. Without Lombok, you write:

  • A no argument constructor for frameworks like Hibernate or Jackson that need to create objects reflectively
  • An all argument constructor for convenient object creation
  • A getter and setter for each field (eight methods total)
  • A toString for logging
  • An equals and hashCode so the class works correctly in collections

That is roughly eighty to one hundred lines of code, nearly all of it generated by your IDE, communicating nothing meaningful about your domain. If you rename a field, you have to remember to update the getter, the setter, the toString, the equals, and the hashCode. Lombok makes all of that automatic and always in sync.


How Lombok Actually Works

This is the part that confuses most developers at first, and it is also a very common interview question.

Lombok is an annotation processor that hooks into the Java compiler. When you run javac (or when your build tool like Maven or Gradle triggers compilation), the compiler runs annotation processors before finishing. Lombok's processor reads your source code, identifies its annotations, and then does something unusual: it directly modifies the Abstract Syntax Tree (the AST) of your code before the compiler turns that AST into bytecode.

Think of the AST as the compiler's internal representation of your source code as a tree of nodes: class declarations, field declarations, method declarations, statements, and expressions. Lombok walks that tree, finds the nodes decorated with its annotations, and injects new method nodes directly into the tree. When compilation finishes, the resulting .class file contains those generated methods as if you had written them yourself.

This is why Lombok is sometimes called a "hack." Standard annotation processors are only supposed to read the AST and generate new source files alongside it. Lombok goes further and modifies the existing tree, which is technically unsupported behavior in the Java specification. In practice it has worked reliably for over fifteen years, but it is important to understand this mechanism because it explains both Lombok's power and its limitations.

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.30</version>
    <scope>provided</scope>
</dependency>

Note the provided scope. Lombok is only needed at compile time. The generated code goes into the bytecode, so there is no runtime Lombok dependency needed.

2. Install the IDE plugin and enable annotation processing:

Your IDE (IntelliJ IDEA or Eclipse) 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

Java 10 introduced the var keyword officially, but Lombok had its own version called val and var long before that.

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 exactly the same way but does not make the variable final, so you can reassign it later.

java
import lombok.val;
import lombok.var;

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 nonnull but is null");
    }
    System.out.println("Creating user: " + username);
}

Key point: @NonNull can only be placed on method parameters and constructor parameters, not on class fields directly. However, when you use constructor generating annotations like @RequiredArgsConstructor, placing @NonNull on a field causes the generated constructor parameter for that field to include the null check.


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 at class level but want to exclude a specific field, annotate that field with @Setter(AccessLevel.NONE):

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 final 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 nonnull but is null");
    }
    this.department = department;
}

Why Are These Three Needed Together?

Different layers of your application need different constructors:

  • Hibernate / JPA requires a no argument constructor to create entities reflectively
  • Jackson (JSON deserialization) typically requires a no argument constructor or an all args constructor with @JsonCreator
  • Your own code often wants an all args constructor for clean object creation
  • Spring can use a required args constructor for dependency injection

Rather than writing all three yourself, let Lombok do it.


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.

Interview question: What is the contract between equals and hashCode? 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() { ... }
}

@Value is ideal for value objects in domain driven design: things like Money, EmailAddress, Coordinate, or DateRange that represent a concept and should never change once created.


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 the private constructor of ServerConfig.


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 FileReader {
    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.

Modern alternative: Java 7 introduced try with resources, which handles this natively. @Cleanup predates try with resources and is less common in newer code. However, it works for any class, not just those implementing AutoCloseable, and you can customize the cleanup method name if close() is not the right call.


Feature 11: @Slf4j for Logging

@Slf4j generates a logger field in your class using SLF4J (Simple Logging Facade for Java). Instead of writing the boilerplate logger declaration yourself, you just annotate the class.

Without Lombok:

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PaymentService {
    private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

    public void processPayment(String orderId) {
        log.info("Processing payment for order: {}", orderId);
    }
}

With Lombok:

java
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PaymentService {
    public void processPayment(String orderId) {
        log.info("Processing payment for order: {}", orderId);
    }
}

Lombok generates the exact same private static final Logger log field. The log variable is available everywhere in the class. Lombok also provides @Log4j2, @Log, @CommonsLog, and other variants depending on which logging framework your project uses.


Feature 12: @SneakyThrows

Java has checked exceptions, which means if a method can throw a checked exception, the compiler forces you to either catch it or declare it with throws. Sometimes this is inconvenient, especially when you are implementing an interface that does not allow throwing checked exceptions.

@SneakyThrows lets you throw a checked exception without declaring it, by exploiting a JVM technicality: the JVM does not enforce the distinction between checked and unchecked exceptions at the bytecode level. Only the Java compiler enforces this rule.

java
import lombok.SneakyThrows;

public class DataLoader {
    @SneakyThrows
    public String loadData(String filePath) {
        return new String(java.nio.file.Files.readAllBytes(
            java.nio.file.Paths.get(filePath)
        ));
        // IOException is checked but we do not declare it
    }
}

Use this sparingly. Checked exceptions exist for a reason: they force callers to consider failure scenarios. @SneakyThrows hides that contract from the caller. Use it only when you are absolutely certain the checked exception cannot occur in practice, or when you are implementing an interface that prevents you from declaring the exception properly.


When NOT to Use Lombok

Lombok has real tradeoffs, and experienced engineers know when to leave it out.

1. When @EqualsAndHashCode Creates Hidden Bugs with JPA

The most common pitfall with Lombok in production is using @Data or @EqualsAndHashCode on JPA entities. JPA entities often have fields like lazily loaded collections or other entities. If those fields are included in equals and hashCode, calling hashCode on a detached entity can trigger lazy loading or throw a LazyInitializationException. The safe approach for JPA entities is to either write equals and hashCode manually using only the entity's natural key, or use a Hibernate specific strategy.

2. When You Need Fine Grained Control

@Data generates everything. If you want only some of those behaviors, the granular annotations give you more control. Blindly using @Data on every class leads to unintended mutability and methods you did not want exposed.

3. When Other Developers Cannot Read the Generated Code Easily

In teams where not everyone is familiar with Lombok, the "magic" can cause confusion during debugging. When a NullPointerException points to line 5 of a class where line 5 is just @Data, tracing the issue requires understanding what @Data generates. Make sure your whole team is comfortable with Lombok before adopting it widely.

4. When You Have Immutability Requirements That @Builder Alone Does Not Enforce

@Builder does not make the class immutable by itself. The generated builder calls a constructor, but if you also have @Setter on the class, consumers can still mutate the object after building. Pair @Builder with @Value or remove setters explicitly when immutability is required.

5. When Using Java Records

Java 16 introduced records as a first class language feature for immutable data carriers. A record gives you a constructor, getters, toString, equals, and hashCode with no annotations at all. If you are on Java 16 or later and just need an immutable data class, a record is cleaner and has no external dependency:

java
public record Point(int x, int y) {}

That single line replaces what @Value used to provide.


Interview Questions

Q: What is Project Lombok and how does it work internally?

Lombok is a Java library that reduces boilerplate code through annotations. It works as an annotation processor that hooks into the Java compiler. Unlike standard annotation processors that only generate new source files, Lombok directly modifies the Abstract Syntax Tree of your code before it is compiled into bytecode. This means the generated methods exist in the compiled .class file as if you had written them yourself, but your source file remains clean and short.

Q: What is the difference between val and var in Lombok?

Both infer the type of a local variable from the initializer expression, so you do not have to write the type explicitly. val makes the variable final so it cannot be reassigned. var does not make it final and allows reassignment. Both only work for local variables inside a block, not for class fields or method parameters.

Q: What is the difference between @Data and @Value?

@Data generates a mutable class: all fields get getters and setters (except final fields which only get a getter), and a required args constructor is generated. @Value generates an immutable class: all fields are made private final, the class itself is made final so it cannot be subclassed, only getters are generated (no setters since fields are final), and an all args constructor is generated. Both generate toString, equals, and hashCode.

Q: What annotations does @Data combine?

@Data is equivalent to @ToString plus @EqualsAndHashCode plus @Getter on all fields plus @Setter on all nonfinal fields plus @RequiredArgsConstructor.

Q: What does @RequiredArgsConstructor generate?

It generates a constructor that takes as parameters only the final fields and the @NonNull annotated fields. For @NonNull fields, it also adds a null check at the top of the constructor. Nonfinal and non-@NonNull fields are not included in this constructor.

Q: What is a common pitfall of using Lombok with JPA entities?

Using @Data or @EqualsAndHashCode on JPA entities is risky because the generated equals and hashCode methods include all fields by default, which can cause problems with lazy loading. Hibernate uses proxy objects for lazy relationships, and including them in hashCode can trigger loading or throw exceptions when the entity is detached from the session.

Q: How is @SneakyThrows able to throw a checked exception without declaring it?

The JVM bytecode does not enforce the distinction between checked and unchecked exceptions. That distinction is enforced only by the Java compiler. @SneakyThrows wraps the method body in a way that the compiler does not see the checked exception at compile time, but the exception propagates normally at runtime. This bypasses the compiler's enforcement but does not change how the JVM handles the exception.

Q: What does @Cleanup generate and what is its modern equivalent?

@Cleanup generates a try finally block that calls close() on the annotated resource when the current scope exits, ensuring the resource is released even if an exception occurs. The modern equivalent is Java's try with resources statement, which was introduced in Java 7 and handles the same use case natively without any library dependency.

Q: Why does IntelliJ show red error lines for Lombok code even though it compiles correctly?

Because the IDE resolves references at edit time based on what it can see in the source code. The Lombok generated methods do not exist in source code. Without the Lombok IntelliJ plugin and annotation processing enabled, the IDE does not know those methods will be generated during compilation. After installing the plugin and enabling annotation processing, the IDE reads what Lombok would generate and provides correct code completion without false errors.

Q: Can you use @Getter and @Setter at both the class level and the field level simultaneously?

Yes. When you use them at class level, they apply to all eligible fields. You can then override at the field level to change the access level or suppress generation entirely using AccessLevel.NONE. The field level annotation overrides the class level annotation for that specific field.


Putting It All Together: A Real World Example

Here is a typical service model class that you might see in a Spring Boot application:

java
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

@Data
@Builder
@Slf4j
public class OrderRequest {
    @NonNull
    private String orderId;

    @NonNull
    private String customerId;

    private String productCode;
    private int quantity;
    private double totalAmount;
    private String shippingAddress;

    public void validate() {
        log.info("Validating order request: {}", orderId);
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        if (totalAmount <= 0) {
            throw new IllegalArgumentException("Total amount must be positive");
        }
        log.debug("Order request {} validated successfully", orderId);
    }
}

Usage:

java
OrderRequest request = OrderRequest.builder()
    .orderId("ORD-1001")
    .customerId("CUST-42")
    .productCode("PROD-Widget")
    .quantity(3)
    .totalAmount(29.97)
    .shippingAddress("123 Main St, Springfield")
    .build();

request.validate();
System.out.println(request); // toString from @Data

What Lombok generates here: a log field, a required args constructor for orderId and customerId with null checks, getters and setters for all fields, toString, equals, hashCode, and the complete builder infrastructure. Your source stays clean and reads like a specification of what the class is, not a wall of mechanical code.


Summary

Lombok solves a genuine problem in Java development. The language has historically required enormous amounts of repetitive code for even simple data classes. Lombok eliminates that repetition by generating code at compile time through AST modification, keeping your source readable and your compiled output complete.

The annotations worth knowing deeply are @Data, @Value, @Builder, @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode, @NonNull, @Cleanup, @SneakyThrows, and @Slf4j. Know what each generates, know how to customize them, and know when not to use them, especially around JPA entities and classes that should be Java records instead.

Lombok is one of those tools that divides developers. Some teams love it and use it everywhere. Others avoid it because of the "magic" and the IDE configuration requirement. Whatever your team decides, understanding how it works and what it generates makes you a better engineer in either case.