Issue with Lombok

Bi-directional relationships

When you have bi-directional associations in your entities, @Data can cause infinite recursion in equals(), hashCode(), or toString() methods, leading to a StackOverflowError.

Example

You have two entities with bi-directional relationship Department and Employee. And you have the below two classes.

@Data
public class Department {
    private Long id;
    private String name;
    private List<Employee> employees; // Bi-directional relationship
}
 
@Data
public class Employee {
    private Long id;
    private String name;
    private Department department; // Bi-directional relationship
}

Lombok generated code

When you use Lombok’s @Data, it automatically generates an equals() and hashCode() method that includes all fields. Let’s take the Department class as example. Lombok will generate a equals() for it behind the scene:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Department that = (Department) o;
    return Objects.equals(id, that.id) &&
           Objects.equals(name, that.name) &&
           Objects.equals(employees, that.employees); 
}

What would happen next?

When your application executes the below code:

department1.equals(department2);

It is going to hit this line:

Objects.equals(employees, that.employees)

That triggers equals() on each Employee. Each Employee has a reference back to the Department. So the Employee.equals() compares its department, which again compares employees, and so on. Ultimately result in a StackOverflowError.

Solution

Other than use @Data which generates many different Lombok annotations. You can use the ones that you need (e.g. @Getter, @Setter, @NoArgsConstructor) and manually implement the equals(), toString() and hashCode() methods if you need them.

The @Builder annotation

Problem 1: Builder ignores initialised collections

Suppose you have the class:

@Getter
@Setter
@Builder
public class User {
    private String name;
 
    // Initialised field
    private List<Event> attendingEvents = new ArrayList<>();
}

Here @Builder does not call the field initialiser. If you call the object with:

User user = User.builder().name("Jason").build();

Then user.attendingEvents will be null, not an empty list, even though you initialised it inline.

Solution

Use Lombok’s @Builder.Default to force initialisation inside the builder.

@Builder
public class User {
    private String name;
 
    @Builder.Default
    private List<Event> attendingEvents = new ArrayList<>();
}

This makes the builder respect the default value.

Problem 2: Conflicts with JPA annotations

JPA entities often use:

  • @CreatedDate
  • @LastModifiedDate
  • @ManyToMany, @OneToMany, etc.

These rely on JPA’s proxy mechanism and lifecycle hooks. But @Builder:

  • Bypasses constructors
  • May cause unexpected nulls or incorrect lifecycle behaviour if combined with things like auditing annotations (@CreatedDate, etc.)
  • Can mess with lazy loading if collections or related entities are initialised improperly

Solution

No good solution as far but avoid using @Builder in JPA entities.


Back to parent page: Java

Java Lombok