Don’t use Lombok

Gonzalo Vallejos
9 min readJun 11, 2018

--

TL;DR: Adds an unknown risk and today are better alternatives.

The first time I touched code with Lombok everywhere I was very confused, it really transformed Java into something else that, based on some opinions, was better or worse. Many years ago when this project started, most people thought the coding experience became better with Lombok, and if I had been a Java developer at that time I would probably have thought the same. However, I didn’t understand the need to transform Java into a fancy language using a non-standard 3rd party library instead of just using a fancier JVM-based language with primary support like Scala or Kotlin. Anyways, I analyzed the codebase and after understanding some Lombok annotations and their generated code I realized that previous developers were not using it correctly and, in fact, introduced a small bug hard to detect.

The @ToString annotation was used everywhere by default in every class, including enums like in the following example:

@ToString
public enum MyEnum {
VALUE_A,
VALUE_B
}

We were writing entries to a database and MyEnum was one of the fields, but instead of seeing values like VALUE_A or VALUE_B, we were seeing the string “MyEnum” in all the values. Previous developers truly believed the problem was in the DAO, but after looking into the delomboked code I realized that this @ToString annotation was generating an implementation like this:

public toString() {
return "MyEnum";
}

Which is returning the name of the type and not the enum value name. This is because the purpose of the generated toString() method is to print the type plus the values of all the fields, something like MyEnum(field1=value1, field2=value2,...), however, here we didn't have any field so just the type was printed. In summary, we didn't need the annotation and it was causing harm instead of adding value.

I know this is just a problem due to misuse of a library and wrong interpretations and there were many ways to prevent it (like a better unit test in the DAO), but the hidden generated code made this problem hard to debug.

After a team discussion, we agreed to move away from Lombok because we believed it was not solving any critical problem. Really at that time, we didn't have very strong arguments to not use it, but we didn't feel comfortable with it so that was good enough to move away: it was more a decision about styling, like choosing between spaces or tabs, or the placement of the brackets.

But the nightmare started when we decided to remove it: the code was very polluted with Lombok annotations and in most of the places it was not really needed, like the example above. The urban myth said that moving away from Lombok was a one-step process using delombok to produce the expanded code to replace your annotated files. However, the generated files were extremely ugly and not following any styling, I don't remember all the problems that we faced but one example was the use of @NonNull annotations that were converted to many lines using if/throw blocks instead of one-line solutions like Guava Preconditions or Validate from Apache Commons. This should have been expected because makes sense to have the generated code with vanilla Java instead of adding more dependencies, but nobody realizes the real work needed to perform this transition until it's needed. We ended modifying the implementation of the annotated files instead of using the delomboked files, which was a lot of work. In this situation, many people might prefer to leave Lombok there, but my main concern was what would happen if eventually a migration is really needed to support a major Java upgrade (right now Lombok is still facing problems with Java 9-11), so we preferred to avoid that concern and solve the problem of boilerplate code later.

The story above is just to describe my short experience with Lombok, but after using other approaches to solve the problems Lombok was made for, I believe that new code should totally avoid it.

The real problem we want to solve

Essentially, what we want to achieve is the reduction of boilerplate lines to have a cleaner code base in order to improve readability and maintainability. This is due to the nature of Java: it requires the definition of methods at compile time so a JavaBean usually needs many method definitions for every member: getters, setters, equals, toString, and maybe some builders, factories or constructors for instantiation. Adding or removing members is even more painful.

A JavaBean has been used as the core unit to define the model domain, and the authors of Lombok had a brilliant idea to create JavaBeans with minimal code and generate the required methods modifying the Abstract Syntax Trees during the annotation processing stage when compiling the Java files in order to inject methods into existing classes. This process has been stated as a hack and sometimes as a trick, but in any case, it is a non-standard approach which means can break after any minor or major JDK change. This risk, however, has been ignored just because there is no way to achieve the same following a standard way, which is true, but the real question should be, Is there any standard approach to solve the same problem in a different way? There are two other libraries trying to answer this question: AutoValue and Immutables, which are often compared to Lombok.

The main difference between AutoValue/Immutables and Lombok is that the first ones are based on interfaces to set the definitions of what will be generated and the result will be a new class implementing the interface, instead of Lombok which injects code inside an existing implementation class. For many people used to work with JavaBeans the first idea is very weird: Why having interfaces for the model domain? But the answer to this is related to very old principles: separation of concerns and minimization of mutability.

Let’s see the following bean as an example:

public class House {
private Door mainDoor;

public void setMainDoor(Door mainDoor) {
this.mainDoor = mainDoor;
}
public Door getMainDoor() {
return mainDoor;
}
}

The main problem here is that an instance of House doesn’t guarantee a non-null mainDoor, even if that is desired, so the code requiring the mainDoor should check if that field is non-null and throw a runtime exceptions saying “I need this to be non-null, but I don’t have any idea why is null”. Adding a constructor with the mainDoor as a parameter doesn’t solve the problem: anyone can add a null value coming from a Door computed by broken logic, and checking for null values in the setter will only change where to throw the runtime exception. The ideal way to prevent these issues is having an instance without mutators and only accessors to prevent these problems at compile time which is usually called an immutable POJO, and the cleaner way to implement it is through an immutable interface (i.e. an interface with only getters).

Let’s redefine our definition of House with an immutable interface:

public interface House {
Door getMainDoor();
}

Then, we can have an implementation like this to set the fields only during instantiation:

public class ImmutableHouse implements House {
private final Door mainDoor;
public ImmutableHouse(Door mainDoor) {
this.mainDoor = checkNotNull(mainDoor);
}
@Override Door getMainDoor() {
return mainDoor;
}
}

If we really need another implementation that requires mutable fields like the bean above, we can just create it by extending our original definition:

public class HouseBean implements House {
private Door mainDoor;
public void setMainDoor(Door door) {
this.mainDoor = mainDoor;
}
public Door getMainDoor() {
return mainDoor;
}
}

Then, you can use this bean only where is needed, while most of the code will just use the interface.

Defining immutable interfaces is a very good practice, and I’m sad that the core Java interfaces were not defined in this way: modern code uses constantly immutable collections and maps but the standard interfaces contain modifiers like put or add, so immutable implementations can only throw an exception if those methods are called which means a misuse can’t be prevented at compile-time. And there is no way to fix this until we travel back in time.

Back to the topic of this entry, If we want to follow this approach with Lombok, we would need to create the interface, the implementation, and annotate the implementation. However, the alternative approaches will make this process simpler: just defining the annotations in the interface will generate the implementation code.

For example, with Immutables we can annotate the interface like this:

@Value.Immutable
public interface House {
Door getMainDoor();
}

This will generate a class ImmutableHouse with a builder to create an immutable House instance, and can be used like this:

Door mainDoor = ...; // Whatever logic to get a Door instance
House house = ImmutableHouse.builder().mainDoor(mainDoor).build();

Of course, the above result can be fully customized: if you want to use a different prefix, if you want to prefix “with” to the builder parameters, etc. The intention of this example is just to show the approach and not what is the best way to use Immutables.

Similarly, if we also need a mutable instance, we can just add an additional parameter to the interface:

@Value.Immutable
@Value.Modifiable
public interface House {
Door getMainDoor();
}

The above code will generate also a ModifiableHouse implementation class where you can change the attributes after the instantiation.

There are many more features added to the generated code, like the generation of toString(), equals(), and serialization properties (which are also available in Lombok with optional annotations), but it also has modern features like the support of Optional return arguments in the getters to make those values optional in the builder.

In summary, the problem of the boilerplate code to define the model domain can be solved with alternatives to Lombok if we want to accept the idea of having immutable interfaces to define our models.

Additional Lombok features

The other Lombok features, beyond the generation of beans, are totally unnecessary and their use only makes your code more coupled to this library. Let’s check what Lombok offers and their relevance:

@NonNull

The basic use of @NonNull is something like this:

public NonNullExample(@NonNull Person person) {
this.person = person;
}

There are already more mature libraries to do the same in a standard way, like using Guava’s Preconditions:

public NonNullExample(Person person) {
this.person = checkNotNull(person);
}

I don’t see too much benefit about using the @NonNull annotation.

@Cleanup

This is not needed today, Java 7 introduced try-with-resources.

@Getter @Setter @ToString @EqualsAndHashCode @NoArgsConstructor @RequiredArgsConstructor @AllArgsConstructor @Data @Value @Builder

As discussed above, there are better alternatives to implement the model domain, and I don’t see any benefit of using them in classes with business logic, especially the constructors if we want to use dependency injection because adding an extra annotation to the constructor (like @Inject) is only supported as an experimental feature subject to change. Furthermore, I don’t see real value in hiding the constructor implementation which can be easily modified by an IDE.

@SneakyThrows

Really the number of cases in which this annotation can be useful is minimal, however, in the majority of the cases is a terrible practice: the fact most of the Java developers don’t like the idea of checked exceptions doesn’t mean we should tricky the compiler to avoid them. To minimize the risk of introducing bad practices we should never use this.

@Synchronized

This feature is just a replacement of the synchronized method to use, instead, a synchronized block with a lock object, which at the end, changes two lines of code by one. I agree in which the annotation makes this cleaner, but I would not use a non-standard library just to reduce one line of code especially if this is not frequently needed, and I don’t want other developers to look for the documentation of an annotation that offers a small benefit.

@log

This is, in my opinion, a very useless annotation: it replaces one line of code by one line of code. Really developers are so lazy to prefer this over creating a static instance? Or someone truly believes this annotation is a significant coding improvement?

@val @var

This is like an attempt to make Java closer to Scala or Kotlin and makes me coming back to my original thought: why not coding in another JVM language instead of adding experimental features to Java? You can even merge Scala and Java code in the same project. I’m not saying you should, but I don’t get the idea of “improving” Java with non-standard features. Furthermore, the var functionality is available natively since Java 10, a version where Lombok is still not working successfully. Do we really need this level of coupling with an extra risk for some syntactic sugar?

Conclusion

Lombok is a smart project supported by a large community and is still the only solution to reduce the boilerplate code of existing implementations by the cost of coupling and an unknown risk due to the use of a non-standard process. However, we can use alternatives based on standard features if we change the paradigm of how to generate the boilerplate code and what we really want to generate.

--

--