Skip navigation and jump to content

Felix Nehrke

I write about software development and software architecture.

deutsch

Problems with Lombok

Almost every Java developer knows Project Lombok. It is one of the most widely used libraries and keeps popping up somewhere. The advantages are obvious and are shown very clearly on the project page. Even so, not everything Lombok does is good and there are some pitfalls using it. I want to dwell a little here on the issues I see in using Lombok. I am not concerned with avoidance, but rather with awareness of weaknesses and pitfalls.

What is Lombok

The first thing I want to do is briefly explain what Lombok is, how it works, and why it was created.

Lombok is mainly a library that can relieve us developers of a lot of work. While it could be argued that this is a goal of most libraries, Lombok is a very special case. It helps us to dramatically reduce the boilerplate code and to concentrate on the application instead.

Boilerplate code is code that is necessary to develop an executable program at all. That means code that does not contribute directly to our use case, but is still necessary. An example of this is the main method, which is always required in Java applications, but not in some other languages.

To illustrate how Lombok helps us, follows here an example with Lombok annotation and one without these simplifications.

with Lombok
@Data
class Product {
  private final String name;
  private BigDecimal price;
}
without Lombok
class Product {
  private final String name;
  private BigDecimal price;

  public Product(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public BigDecimal getPrice() {
    return price;
  }

  public void setPrice(BigDecimal price) {
    this.price = price;
  }

  @Override
  public String toString() {
    return "Product(" + name + ", " + price + ")";
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) return true;
    if (!(obj instanceof Product)) return false;
    Product other = (Product)obj;
    return name.equals(other.name) && Object.equals(price, other.price);
  }

  @Override
  public int hashCode() {
    Object.hash(name, price);
  }
}

As you can see, the effort to get a comparable class without Lombok is huge. So, in fact, a lot of the work is being done for us and Lombok can apparently keep its promise.

The crux is in the detail

At first glance, everything that Lombok does here looks very good, but it also hides a lot and raises questions.

  • How exactly do equals() and hashCode() behave?

  • What happens if I want to implement Comparable?

  • Are there any restrictions on using other techniques?

  • What alternatives are there?

In addition to these questions, there is another problem, namely simplicity. The extremely simplified notation suggests that the code is simple even though it is not. As I said, Lombok only hides the complexity, but it doesn’t magically disappear. All the peculiarities and pitfalls of Java are still there. To make this clear, I like to show the following example:

@Data
class Example {
  private final UUID id;
  private String value;

  public static void main(String[] args) {
    Example example = new Example(UUID.randomUUID());
    example.setValue("foo");
    Set<Example> exampleSet = new HashSet<>();
    exampleSet.put(example);

    assert exampleSet.size() == 1;
    assert exampleSet.iterator().next().equals(example);
    assert exampleSet.contains(example); (1)

    example.setValue("bar");

    assert exampleSet.size() == 1;
    assert exampleSet.iterator().next().equals(example);
    assert exampleSet.cotains(example); (2)
  }
}
1All tests work.
2The test suddenly fails in this line!

The reason for the error can be found in the implementation of the hashCode() method. Lombok has overwritten this for us and calculates a hash from id and value. But we don’t want that here, because we already have a unique ID, namely the id. Accordingly, only this field should be used for the hash. In order for Lombok to behave correctly here, we have to tell Lombok that we only want to consider id. This changes the class as follows:

@Getter
@Setter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
class Example {
  private final UUID id;
  @EqualsAndHashCode.Exclude
  private String value;
}

As we can see, only this small change let the need for annotations explode. Unfortunately, that’s only the obvious deterioration; the implied effects are much more serious. Because the far bigger problem is the knowledge that you have to bring with you in order to handle this situation correctly. To be able to understand this change you have to:

  • Understand how equals and hashCode work.

  • Know what all the annotations are doing.

  • Know what the generated code will look like.

  • Even know all the annotations.

All in all, Lombok does not save us from having to deal with the internals. It just makes the code a little clearer, but not easier to understand.

Standard-interfaces in the face of Lombok

The decision to go to Lombok can have other implications as well. For example, the method compareTo() from the Comparable interface relates closely to the equals().

It is strongly recommended, but not strictly required that (x.compareTo(y)==0) == (x.equals(y)).

— Java® Platform, Standard Edition & Java Development Kit
Version 15 API Specification

So if I implement Comparable and annotate @EqualsAndHashCode, I can easily create a conflict. A conflict that I cannot easily understand because the code cannot be seen. Therefore, it is essential to observe the documentation in order to avoid stupid errors for standard interfaces, but also for all others.

This advice also applies to things like JPA and other widely used and important techniques. For example, Hibernate requires an empty constructor, but this can be easily lost with Lombok. On the other hand I’m a big fan of static factories, or the builder pattern to build entities.

Fortunately, Lombok also offers us some handy help for the builder pattern:

@Builder
@Entity
class Example {
  @Id private UUID id;
  private String value;

  public static void main(String[] args) {
    Example example = Example.builder()
      .id(UUID.randomUUID())
      .value("foo")
      .build();
  }
}

This notation is again very simple and can be done quickly. However, Hibernate is now complaining because it cannot find a suitable constructor. Again, the search begins with a little misunderstanding, because Lombok embarrasses a constructor here, as delombok reveals. However, as mentioned, Hibernate is not looking for just any constructor, but an empty one. In short, we still have to annotate that an empty constructor is required, so that the class looks like this:

@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
class Example {
  @Id private UUID id;
  private String value;
}

The problem and alternatives

One or the other has probably already noticed that the problems mentioned can also be generalized. Many times the problem is not cause by Lombok itself, but rather state handling problems with Lombok.

This is my main criticism of the use of Lombok. Often the usage seems to follow the famous nail proverb instead of being really necessary. In other words, Lombok is often simply the wrong tool or solves problems that don’t even exist.

If your only tool is a hammer then every problem looks like a nail.

— common saying

Well what alternatives would we have instead of using Lombok? I don’t think we have to do without Lombok, but we should look for and find alternatives that do their job better. The Auto framework, for example, is simply more consistent than Lombok and clearly defines its limits. AutoValue is ideal for value objects and is much more precise than Lombok.

Example of AutoValue
@AutoValue
abstract class Example {
  abstract UUID id();
  abstract String value();

  static Example of(UUID id, String value) {
    return new AutoValue_Example(id, value);
  }
}
A value object is not an entity!

In addition, our environment is constantly evolving, which means that other languages such as Scala or Kotlin are available to us. These languages partially address the same problems as Lombok, but with more precisely defined barriers. With these more precise definitions, it is easier to write better code and not so easy to run into traps. However, you don’t even have to look at other languages, you can also look at the development of Java. From version 15 we can program the example using a record. However, these advances always only affect one aspect of Lombok, which is why we cannot replace everything, let alone should.

record Example(UUID id, String value) {}

Conclusion

Lombok is one of the most popular tools in the Java world, and it will be for a while. Nevertheless, it is worth taking a look at other solutions, as these often solve their tasks very well and precisely. In addition, Lombok in no way relieves the developer from worrying about his code. On the contrary, I think it’s even dangerous to use Lombok without knowing what the same code would look like without it. Nevertheless, Lombok has its raison d’être and sometimes accelerates development enormously. In the end, however, I am always happy when I see that a project does without Lombok. Still, I like working with Lombok because sometimes it just speeds up development. In the end, I am always happy when I see projects that do without Lombok.