Lombok: The Batman of the JRE that saved Java from Boilerplate
Java has been the backbone of enterprise development for decades, but let’s face it: writing getters, setters, and constructors is about as exciting as watching paint dry.
Java is a conservative language and, luckily for us, doesn't want to introduce big breaking changes.
DX is better in modern languages
This comes with a problem: the developer experience in Java is not as good as in modern languages (Kotlin, yes, I like it).
Java has a lot of boilerplate that comes from the 'design' adopted 20+ years ago. Kotlin has been built on the Java experience and has been less conservative. Often the youngest developers don't understand why we need to use the 'old beast' Java.
Java is improving quickly but still not perfect ... until then when we have Lombok
In many projects, I’ve seen that Lombok significantly reduces the pain of developing in Java compared to Kotlin and we have fewer reasons to switch.
Not all Java developers like Lombok, it's an annotation processor that uses non-public compiler APIs, fair enough.
Like Batman, Lombok operates in the shadows — effective, but not always accountable.
The good news is that Java is improving quickly: records, pattern matching, and sealed classes already replace some Lombok use cases.
The functions of Lombok that we use in a Java project
@Data (use with caution)
@Data: we use this a lot to reduce the boilerplate of the 'old' getters and setters. Similar todata classin Kotlin. You can use it on DTO-like model classes (not entities!) by defaultequals/hashCodewith mutable fields breaks Hibernate identitytoStringcan trigger lazy loading- Avoid on entities!!! equals() / hashCode() includes mutable fields, breaks Hibernate proxy equality, toString() includes lazy fields and could load all your data
@RequiredArgsConstructor (recommended for Spring)
@RequiredArgsConstructor, if you are using Spring you should use this on all your@Componentand@Serviceto declare the injections.
Some LLMs still suggest@Autowired:|, as you know you should use the constructor to inject the components. With @RequiredArgsConstructor, you can inject dependencies simply by declaring them as final fields:
private final MyService myService
@Slf4j (convenient for logging)
@Slf4j: we use when a log is needed it's very useful to avoid theLoggerinstantiation in the class. With this annotation you can simply log with:
@Slf4j
public class MyClass {
void example() {
log.warn("example");
log.info("example");
}
}
@With ... and you will copy the objects like in Kotlin
@With: if you use Kotlin you will love this. You can work with immutable classes (e.g.@Value) and copy the original class modifying only one (or more) field(s). This is a common practice in Kotlin with the function copy
@Builder (recommended for complex initializations)
@Builder: we use it for classes that contain many (10+ fields) or that requires some complex initialisations.
Other useful annotations used to reduce boilerplate
@Getter/@Setter/@ToString/@EqualsAndHashCode/@NoArgsConstructor
we use this combination (or a part of it) for@Entity, as said we prefer to avoid@Datafor Entities.
Lombok and JPA
In our projects, we don't allow the use of @Data on entities, there are risks if not handled correctly.
this is an example of a simple entity:
@Data // DON'T USE IT ON ENTITIES, better use @Getter @Setter @NoArgsConstructor
@Entity
public class MyEntity {
@Id
private Long id;
private String name;
}
If we delombok the class to see what happened behind the scenes we see that the equals include all the fields, it should not be the case for an Entity, the id should be used to evaluate the equality.
// @Data also generates these, but be careful with them in Entities:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyEntity myEntity = (MyEntity) o;
return Objects.equals(id, myEntity.id) &&
Objects.equals(name, myEntity.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
Should we have inside the @Entity a @OneToMany relationship, the risks would increase:
@OneToMany(mappedBy = "myEntity")
private List<Comment> comments = new ArrayList<>();
// Generated by @ToString
// THIS IS THE DANGEROUS PART, the lazy loading of comments will be executed!
@Override
public String toString() {
return "MyEntity(id=" + this.getId() + ", name=" + this.getName() + ", comments=" + this.getComments() + ")";
}
Summary: When Lombok makes sense and when to avoid it
It shines for
- DTOs
- Internal services
- Prototypes and fast-moving codebases
Be careful (avoid) with
- JPA entities
- Public libraries
- Long-lived APIs
Why Lombok Is Actually Controversial (and it gets some hate)
The Abstract Syntax Tree (AST) Hijack
The standard Java compilation process follows a specific pipeline: Source Code (.java) → Parse (AST) → Enter (Symbol Table) → Annotation Processing → Analyze → Generate (Bytecode).
According to the Java compiler and annotation processing contract, annotation processors are only allowed to generate new files. They are strictly forbidden from modifying existing ones. Lombok chooses to ignore this rule.
Lombok hooks into the JSR-269 Annotation Processing API, but instead of creating a UserDTO_Generated.java file, it uses internal, undocumented APIs (like com.sun.tools.javac) to mutate the Abstract Syntax Tree (AST) in-memory.
When the compiler builds the tree for your class, Lombok's handlers find nodes annotated with @Data.
It programmatically inserts new "nodes" into that tree—nodes representing your getters, setters, and constructors.
By the time the compiler reaches the "Generate" phase to write the .class file, it sees those methods as if you had typed them yourself. It translates the modified AST into bytecode.
From a strict JLS perspective, this approach is not allowed. Lombok intentionally trades spec compliance for usability (ref. https://docs.oracle.com/en/java/javase/25/docs/api/java.compiler/javax/annotation/processing/Filer.html)
Lombok works remarkably well in practice, but it does so by relying on implementation details of javac, which is why some teams consider it a long-term risk.
Here is an interesting list of libraries that use the Annotation Processing API correctly (and some 'bad guys').
https://github.com/gunnarmorling/awesome-annotation-processing?tab=readme-ov-file
Don't worry, Spring and Hibernate are good guys :D
Verdict
Maybe Lombok didn’t save Java, but it is helping Java to survive until the language catches up. Whether you still need it depends on how much magic your team is willing to accept.