Why I Believe Lombok Should Be Discarded from Java Projects
Hello, today’s article tackles a seemingly unpopular view, and I am sure it will meet some resistance. Just because something is technically feasible doesn’t automatically endorse its utility or suitability. Hence, I will attempt to substantiate why I believe using Lombok could detrimentally affect your code.
Unveiling the Magic: Understanding Project Lombok
Before delving into the less popular details, let me offer a concise explanation of how the Lombok library functions.
Project Lombok acts as a library that injects code into a class at compile time, which might appear almost magical. To comprehend its operations, understanding the Java compilation process is essential. Java compilation involves three main stages (Figure 1) : Parse and Enter, Annotation Processing, and Analyze and Generate, as depicted in the following diagram:
Figure 1 – Abstract Syntax Tree (AST)
-
Parse and Enter: Here, the compiler converts source files into an Abstract Syntax Tree (AST). Errors are only thrown for invalid syntax, not for incorrect class or method use.
-
Annotation Processing: During this phase, custom annotation processors validate classes or generate new resources like source files. This may trigger a new compilation cycle if new sources are generated.
-
Analyze and Generate: In this final stage, the compiler produces bytecode from the AST, checking for broken references, verifying logical flows, performing type erasure, and desugaring syntactic sugar.
Project Lombok operates as an annotation processor, modifying the AST by injecting new methods, fields, or expressions. Unlike typical processors that generate new sources, Lombok alters existing classes, a distinction that enables it to impact the generated bytecode directly.
AnnotationProcessor
introduced in J2SE 1.5 cannot make changes to existing files. It could only create new files or bytecode. This makes lombok’s
implementation intriguing, since they use AnnotationProcessor
to modify the existing java class files, during the compilation phase. Here is an overview of the
compilation process with Lombok (Figure 2).
Figure 2 – Compilation process and Lombok
The Case Against Lombok
After understanding the magic behind Lombok, let’s delve into the reasons why I believe it could be detrimental to your codebase.
Increased Compilation Time
Lombok’s operations at compile time inevitably lengthen the compilation process, particularly pronounced in larger codebases due to the increased AST management required.
Misplaced Sense of Benefit
Lombok offers various annotations which may give an illusion of solving fundamental programming challenges. I will discuss some of these annotations which I meet frequently in the codebase.
@Builder
annotation
The @Builder
annotation in Lombok simplifies object creation through the builder pattern, adding a layer of convenience that is initially appealing. Consider this example:
1@Data
2@Builder
3public class Course {
4 public enum Type {
5 ONLINE,
6 ONSITE;
7
8 @JsonValue
9 @Override
10 public String toString() {
11 return super.toString().toLowerCase();
12 }
13 }
14
15 private long id;
16 private Type type;
17}
And its usage:
1public class CourseCreator {
2
3 public static Course createCourse(Enrollment enrollment, Registration registration) {
4 Course.Type courseType = enrollment.getVenue().equals(registration.getVenue()) ? Course.Type.ONSITE : Course.Type.ONLINE;
5
6 return Course.builder()
7 .id(enrollment.getId())
8 .type(courseType)
9 .build();
10 }
11
12 public static void main(String[] args) {
13
14 Registration registration = new Registration();
15 Enrollment enrollment = new Enrollment();
16 Course course = createCourse(enrollment, registration);
17 System.out.println(course);
18 }
19}
While the builder pattern is efficiently implemented, crucial questions about the integrity and validity of the objects being created are raised.
What type of course are we instantiating if we omit the .type()
in the builder?
This line of code would compile, but it leaves us questioning: What type of course have we actually created? Is this a valid course instance?
Course.builder().id(1L).build();
These concerns suggest that developers, perhaps swayed by the convenience of annotations, might overlook thorough domain modeling necessary for maintaining business logic integrity. Instead of letting Lombok dictate our design, a more considered approach ensuring alignment with business requirements is crucial.
Consider adjusting the implementation to ensure that any course creation is clear and constrained within the business context:
1@Data
2public class Course {
3 private enum Type {
4 ONLINE,
5 ONSITE;
6
7 @JsonValue
8 @Override
9 public String toString() {
10 return super.toString().toLowerCase();
11 }
12 }
13
14 public static Course online(long id) {
15 return new Course(id, Type.ONLINE);
16 }
17
18 public static Course onsite(long id) {
19 return new Course(id, Type.ONSITE);
20 }
21
22 private long id;
23 private Type type;
24
25 public boolean isOnline() {
26 return Type.ONLINE.equals(this.type);
27 }
28
29 public boolean isOnsite() {
30 return Type.ONSITE.equals(this.type);
31 }
32}
By redesigning the class:
1public class CourseManagement {
2 public static Course createAppropriateCourse(Enrollment enrollment, Registration registration) {
3 return enrollment.getVenue().equals(registration.getVenue()) ?
4 Course.onsite(enrollment.getId()) :
5 Course.online(enrollment.getId());
6 }
7
8 public static void main(String[] args) {
9 Registration registration = new Registration();
10 Enrollment enrollment = new Enrollment();
11 Course createdCourse = createAppropriateCourse(enrollment, registration);
12
13 System.out.println(createdCourse);
14 }
15}
The revised design ensures that the creation of Course
objects is explicit and foolproof, reflecting the constrained choices
inherent in the domain and eliminating ambiguity.
Moreover, by making the Type
enum private and providing clear, explicit methods like isOnline()
and isOnsite()
, we ensure that only valid states
are exposed and manipulated, safeguarding the domain integrity.
Through this thoughtful restructuring, we demonstrate that while tools like Lombok can significantly reduce boilerplate, they are not substitutes for careful design and a deep understanding of the domain. It underscores that Lombok should be employed judiciously, complementing rather than overshadowing robust architectural practices. This ensures that the elegance of our code does not come at the expense of its correctness and clarity.
- Overreliance on
@Setter
and@Getter
The argument that getters and setters reduce boilerplate falls short when Java offers alternatives like the Record classes from Java 14.
1@Data
2public class Movie {
3 private String title;
4 private int releaseYear;
5}
6
7// Can be replaced with:
8public record Movie(String title, int releaseYear) {}
- Superintendent
@NonNull
Having null
in your code - aside from inputs is generally considered problematic and is often indicative of deeper design issues.
The prevalent advice is to avoid returning null
whenever possible. Instead, opt for alternatives such as returning non-null collections,
utilizing null objects, or throwing exceptions to signify unusual or exceptional conditions. This strategic avoidance means null checks become
redundant in most parts of your code.
To distance from Lombok’s @NonNull
annotation and ensure robustness in Java natively, the Objects.requireNonNull()
method from the java.util.Objects
class is incredibly useful.
This method streamlines null
checking by ensuring that an object is not null, and it throws a NullPointerException
with a clear message if it is.
This explicit exception-throwing mechanism prevents latent null-related bugs from surfacing in runtime, promoting earlier detection during
the development cycle. Here’s an example showing how this method can replace Lombok’s functionality
Using Lombok’s @NonNull
:
1public class NonNullExample {
2 private Student student;
3
4 public NonNullExample(@NonNull Student student) {
5 this.student = student;
6 }
7}
Equivalent pure Java approach:
1import java.util.Objects;
2
3public class NonNullExample {
4 private Student student;
5
6 public NonNullExample(Student student) {
7 this.student = Objects.requireNonNull(student, "Student cannot be null");
8 }
9}
This transition to native Java handling enhances code transparency by making the null-check explicit, which is advantageous for code maintenance and understanding.
Constructor Flexibility and Reusability
Constructors play a critical role in how classes interact within your software architecture. A well-designed class should have a variety of constructors that accommodate different use cases, promoting reusability and flexibility. If your constructors merely replicate field assignments, the underlying issue isn’t the need to write boilerplate code; rather, it’s the risk of fostering a non-reusable and inflexible design that Lombok cannot rectify. Proper constructor design allows a class to be integrated and utilized in a multitude of scenarios, enhancing the overall robustness and adaptability of your codebase.
Evaluating Boilerplate Code: The Lure of Lombok versus Modern Java Features
Lombok’s popularity predominantly stems from its ability to reduce boilerplate code, particularly in domain-specific classes like transfer and data objects. While Lombok effectively diminishes the visible clutter by auto-generating necessary code like getters, setters, equals, hashCode, and toString methods, this convenience might obscure potential pitfalls. However, with the advent of Java Records introduced in Java 14, there is a preferable alternative that natively supports the concise declaration of immutable data carriers. Most integrated development environments (IDEs) are also equipped to automatically generate these boilerplate codes with minimal user input, offering a balance between Lombok’s automation and the control of traditional Java coding.
Compatibility Concerns
Project Lombok’s dependency on the underlying Java version poses a significant compatibility risk. As Java evolves, the Abstract Syntax Tree (AST)
structure and its interpretation could change, necessitating continuous updates to Lombok to ensure compatibility. This creates a
fragile dependency where upgrading to a newer Java version could potentially break your build if Lombok is not simultaneously
updated to support these changes. The reliance on unofficial or private APIs to modify class definitions further exacerbates this
issue because these APIs could be restricted or altered in future Java releases, threatening Lombok’s long-term viability.
Java Standards
Using Lombok can lead to complications when building projects with tools that only use standard Java compiler options. For instance, if your code utilizes getters and setters generated by Lombok, compiling directly with javac without Lombok pre-processing could result in errors indicating missing methods. While some may regard Lombok’s capability to inject code as a clever “trick”, it’s essential to critically assess the associated risks and alternatives. The core of the issue lies in the fact that Java’s annotation processing specification does not officially support modifying existing classes during compilation. Relying on these unofficial techniques makes Lombok vulnerable to future Java updates that could potentially disrupt or disable its functionalities.
Ultimately, these considerations underscore the importance of evaluating not just the immediate benefits of tools like Lombok but also their long-term implications on maintainability, compatibility, and alignment with Java standards. As Java continues to evolve, the reliance on stable, standardized features becomes increasingly critical for ensuring the sustainability and reliability of your software projects.
Conclusion
Lombok might seem like a handy shortcut for Java development, but it shifts Java code into a domain-specific version I like to call “Lombok Java”. It’s essential to realize that relying excessively on Lombok can obscure the Java essence, potentially leading to code that is less robust and harder to manage without Lombok’s crutches.
If over reliance on Lombok is the solution to managing your codebase, it might be time to reevaluate the underlying architecture and practices. The true strength of Java lies in its clarity and structure, not in the shortcuts provided by external libraries.
Given the chance, I would choose to discard Lombok from my projects.