DEV Community

Cover image for Why I Believe Lombok Should Be Discarded from Java Projects
Ivelin Yanev
Ivelin Yanev

Posted on

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:

Abstract Syntax Tree (AST)

Figure 1 – Abstract Syntax Tree (AST)

  1. 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.

  2. 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.

  3. 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).

Compilation process and Lombok

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:

@Data
@Builder
public class Course {
    public enum Type {
        ONLINE,
        ONSITE;

        @JsonValue
        @Override
        public String toString() {
            return super.toString().toLowerCase();
        }
    }

    private long id;
    private Type type;
}
Enter fullscreen mode Exit fullscreen mode

And its usage:

public class CourseCreator {

    public static Course createCourse(Enrollment enrollment, Registration registration) {
        Course.Type courseType = enrollment.getVenue().equals(registration.getVenue()) ? Course.Type.ONSITE : Course.Type.ONLINE;

        return Course.builder()
            .id(enrollment.getId())
            .type(courseType)
            .build();
    }

     public static void main(String[] args) {

        Registration registration = new Registration(); 
        Enrollment enrollment = new Enrollment();
        Course course = createCourse(enrollment, registration);
        System.out.println(course);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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:

@Data
public class Course {
    private enum Type {
        ONLINE,
        ONSITE;

        @JsonValue
        @Override
        public String toString() {
            return super.toString().toLowerCase();
        }
    }

    public static Course online(long id) {
        return new Course(id, Type.ONLINE);
    }

    public static Course onsite(long id) {
        return new Course(id, Type.ONSITE);
    }

    private long id;
    private Type type;

    public boolean isOnline() {
        return Type.ONLINE.equals(this.type);
    }

    public boolean isOnsite() {
        return Type.ONSITE.equals(this.type);
    }
}
Enter fullscreen mode Exit fullscreen mode

By redesigning the class:

public class CourseManagement {
    public static Course createAppropriateCourse(Enrollment enrollment, Registration registration) {
        return enrollment.getVenue().equals(registration.getVenue()) ? 
            Course.onsite(enrollment.getId()) : 
            Course.online(enrollment.getId());
    }

    public static void main(String[] args) {
        Registration registration = new Registration();
        Enrollment enrollment = new Enrollment();
        Course createdCourse = createAppropriateCourse(enrollment, registration);

        System.out.println(createdCourse);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

@Data
public class Movie {
    private String title;
    private int releaseYear;
}

// Can be replaced with:
public record Movie(String title, int releaseYear) {}
Enter fullscreen mode Exit fullscreen mode
  • 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:

public class NonNullExample {
    private Student student;

    public NonNullExample(@NonNull Student student) {
        this.student = student;
    }
}
Enter fullscreen mode Exit fullscreen mode

Equivalent pure Java approach:

import java.util.Objects;

public class NonNullExample {
    private Student student;

    public NonNullExample(Student student) {
        this.student = Objects.requireNonNull(student, "Student cannot be null");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)