skip to content
blog.metters.dev

Lessons learnt from introducing Lombok

/ 5 min read

Some years ago, I joined a Java project as an (external/consulting) developer. The code base was huge, so naturally there was a lot of boilerplate code. Of course, I suggested to introduce Lombok and the proposal was accepted. This was to the surprise of my other external colleagues on the same project, as they have tried this before, but it was declined due to reasons unknown to me.

I was tasked with implementing the necessary changes. Though not everything went as expected, I still learnt quite some things about introducing change to (legacy) software projects.

TL;DR Lessons learnt

  1. No change is trivial
  2. Fewer LOC + more annotations != better readability
  3. Acceptance is crucial
  4. Library code also comes with bugs
  5. Avoid ‘big bang’ rollouts
  6. Tests are invaluable
Never heard about Lombok? Click here, the next section got you covered.

What is Lombok?

One complaint that arises often about java is the big amount of boilerplate code generated. Project Lombok tries to ease that pain.

Code usually is edited few times, but read many times. Reducing the amount of LOC in a POJO helps to understand the code faster. Lombok is a compile-time-processor: the annotations will be transformed into all the boilerplate code. The developer does not have to see all those getters, setters, and constructors anymore, so they are just added during compile time.

POJO without Lombok

public class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

POJO annotated with Lombok

@AllArgsConstructor
public class Person {
@Getter @Setter
private String firstName;
@Getter @Setter
private String lastName;
@Getter @Setter
private int age;
}

Much better, isn’t it? This can be even further shortened to

@AllArgsConstructor
@Getter @Setter
public class Person {
private String firstName;
private String lastName;
private int age;
}

or, if you do not mind having hashCode() and equals() automatically generated as well

@Data
public class Person {
private String firstName;
private String lastName;
private int age;
}

Lessons learnt

As already mentioned, not everything went as smoothly as I had expected. There are some pitfalls, one must consider when introducing Lombok. And fortunately there are things to learn about change-management in general!

No change is trivial

Even though “never change a running system” is too dogmatic, it is popular for a reason: The code was old, but it ran. There was a lot of potential for improvement, so it was no mistake to do this. Nevertheless, it is a good idea to step back and wonder whether it really is a good idea to change a (running) system. Replacing getters and setters with annotations in a huge code base is a boring task. Boring enough to make mistakes and introduce errors. Even if done with the help of search-and-replace, which carries its own risks.

Fewer LOC + more annotations != better readability

The example code snippets above are artificial with the purpose to illustrate how Lombok helps to reduce standard plain Java boilerplate code. As soon as there are other frameworks that use annotations (i.e. Hibernate) the amount of annotations gets out of hand quickly. In such cases readability might suffer more than improve. Also, such setups are rather error-prone.

Acceptance is crucial

Unfortunately, the change was not welcomed or at least accepted by all team members. This lead to conflict. The whole team must be behind it and accept this change, otherwise it is better to not do it at all.

Library code also comes with bugs

After the rollout, bug reports came in and the analysis of a more senior developer indicated that the generated methods equals() and hashCode() were buggy (it did not work well with another dependency, iirc). This was unexpected and the possibility should be taken into account when using library functions. We fixed it by reverting the change in the affected classes.

Unfortunately, I did not open an issue in GitHub back then, but I wish I did.

Avoid ‘big bang’ rollouts

I rolled out all changes at once instead of successively merging smaller chunks. This was a mistake, because at the same time other developers continued to work on the code base, while I was touching nearly all classes. Quite some time was spent on merging all changes together, since the code base was huge.

Please note, I do not intend to say that ‘big bang’ rollouts are to be avoided at all cost. However, in this particular scenario this kind of rollout caused unnecessary pain. Also, I could have rebased my working branch more often—another mistake I learnt from.

Tests are invaluable

The risks coming from the two previously mentioned points can be mitigated by having high test coverage with proper end-to-end or integration tests.1 Tests prove the (tested!) behaviour did not change after editing the code.

Additional resources

Footnotes

  1. I cannot believe I have to mention this: code coverage alone does not guarantee correct behaviour or bug-free code!