Executive summary

This blog describes briefly the most commonly used annotations of the code generation tool Lombok which shields the Java developer from having to generate or write so called ‘boilerplate code’. It begins with explaining that term and the problems it may cause, continues with a brief setup description of a Maven based Java project that incorporates Lombok and then display some useful annotations.

These annotations are grouped after function such as object creation, field access, some methods of the Java top class Object, convenience methods and a useful builder. The blog is concluded with tips on how to use Lombok in other types of Java objects, some pros and cons and finally my personal experience from using the library.

Introduction

I’m sure many of us developers have been there - maybe changing a data transfer object by adding a 23:rd property and generating the methods needed for it to play along, or sighing over the 17.th constructor call in a unit test that only requires half of the properties as non null for the test to go through.

There’s tools out there to that help developers in these situations and this blog post is an essay to briefly present one such tool called Lombok.

Intended audience

Intended audiences are developers but also generally interested parties. Some basic knowledge of Java or another similar programming languages might come in handy. If you want to try it out for yourself some knowledge of Maven and a code studio will be helpful.

Boilerplate code

Wikipedia defines boilerplate code as:

“In computer programming, boilerplate code, or simply boilerplate, are sections of code that are repeated in multiple places with little to no variation. When using languages that are considered verbose, the programmer must write a lot of boilerplate code to accomplish only minor functionality.”

Source https://en.wikipedia.org/wiki/Boilerplate_code 2024-10-24

Enter Lombok

In later years there has been a trend towards reducing the need for boilerplate coding in modern languages such as for example Scala and Kotlin. One has strived to spare the developer from having to write or manually generate (most IDEs have generating capabilities for boilerplate) repetitive code that simply has to be there for things to work.

The code that has to be there

In Object Oriented Programming it is considered good practice to encapsulate and hide an object’s properties from the outside world. The access to and change of a data field must be controlled. Imagine a Person class with an age property - good practice is to ensure this age field can only be accessed through a couple of methods, named getAge() and setAge() by convention (isXXX() for boolean properties). This kind of methods are called ‘accessors’, and they are members of a group of methods prescribed in the Java Bean standard which dates back to 1996. On top of this, the Java arch superclass of all objects, Object, also defines a set of methods that can be regarded as boilerplate as they more often than not need to be overridden for classes to function properly. Lombok supports most, if not all, of these.

Current state of Java

Recent versions Java has taken steps to reduce boilerplate coding through the introduction of the immutable record data type. Records get the necessary methods generated under the hood, but other than that we’re not quite there yet, and Plain Old Java Objects still need accessors and other cook book stuff. This is where Lombok can help clear the way quite a bit.

Adding Lombok to a Maven project

Incorporating Lombok in your project should be simple. Create a basic Java project with Maven and add the following dependencies and configuration. The Maven compiler plugin may need the annotation processor path to function properly.

<project>
  <!-- other stuff -->
  <dependencies>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.30</version>
    </dependency>
    <!-- other stuff -->
  </dependencies>

  <build>
    <!-- other stuff -->
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <annotationProcessorPaths>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <version>1.18.30</version>
            </path>
          </annotationProcessorPaths>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Also, under the POM build/plugins section, add Lombok as an annotation processor.

If you use IntelliJ it generally also wants you to activate annotation processing – it either warns about this or you do it yourself by going to ”Settings” and in the search field look for ”annotation”. There is also a Lombok plugin for the IntelliJ IDE and most certainly for other popular IDEs.

Let’s get started

As Lombok is rather extensive with lots of both production ready and experimental features I will stick to the most commonly used parts of the library. Lombok works through annotations and generated code. One caveat is that this code is generated at compilation time and weaved into the Java .class files which means you won’t be able to debug what Lombok produces.

This may result in a sense of loss of control, but the solution is simple: just implement the method yourself and that version will overshadow the Lombok generation and will be debug-able as any other code - and you’ll be back in the driver’s seat.

Also, to really calm yourself, you can use the ‘delombok’ plugin to remove all annotations and replace it with corresponding code. The ‘delomboking’ procedure is well documented but out of scope for this blog. Just be aware there’s no way back once you’ve done it…

I will go through the most common annotations starting with object construction and copying and then continue with accessors, followed by annotations to override a couple of Object’s most important methods, then a couple of convenience features and lastly the Builder,

Object construction

@NoArgsConstructor

Lombok offers three different flavors of constructor annotations whereof the default ‘no argument’ one is the first. Lombok also offer the possibility to tweak the generated code by adding configuration to the annotation. For example: maybe you’re tired of repeatedly making the default constructor private in singleton scenarios, and Lombok offers an answer to that. I won’t go through all the configuration options there are in this blog but recommend you having a look at the Lombok documentation.

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MySingleton {
    // static methods here
}
@AllArgsConstructor

The name tells us what it does. Use as is or add a factory method by entering the configuration ‘staticName’.

@AllArgsConstructor(staticName = "of")
public class AllArgsPerson {
    private int age;
    private final String socialSecurityNumber;
    private boolean alive = true;
}
// Usage:
var person = AllArgsPerson.of(42, "121212-1212", false);
@RequiredArgsConstructor

This constructor only deals with the required properties of a class, i.e. the final fields and fields marked as @NonNull (more about that later). Note that already initialized final/@NonNull fields are omitted from the generated constructor.

@RequiredArgsConstructor
public class Person {
    private final int age = 42;
    private final String socialSecurityNumber;
    // ...
}
// Usage:
var person = new Person("121212-1212");

Using combinations of these should be mostly hassle-free, but when constructors do conflict and you get plugin- or compilation errors - be pragmatic and don’t do what this example says. Instead implement your constructors yourself as it probably means less work than finding the allowed combination of properties…

// Don't do this
@RequiredArgsConstructor(staticName = "construct")
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class SpecialPerson {
    // ...
}
@With

Squeezing in copying under the creation umbrella: generate a copy constructor for a given property. Lovely when setting up test data where you might need a bunch of slightly different objects. I’ve seen it misused as well (and probably have some guilt myself) through chaining of more than two with methods as each call creates an object that is then discarded on subsequent with calls. In those cases I would probably recommend making a custom copy constructor for the fields in question.

public void  Person() {
    @With
    private String fullName;
    private int age;
}
// Usage:
var jane = Person.builder().fullName("Jane Doe").age(42).build();
var john = jane.withFullName("John Wayne");

Accessors

@Getter and @Setter

Applicable at the field and class level these two are possibly the least invasive but also the most time saving of all Lombok annotations – they simply generate accessors (getXXX/isXXX, setXXX) for you. Note that final properties will not have setters generated. If you want to start somewhere these are good candidates.

@Setter
@Getter
@AllArgsConstructor
public class Person {
    private int age;
    private final String socialSecurityNumber;
    // ...
}

Customizing the accessors creation can be done in a couple of ways: annotate each field individually which may result in a bit of cluttered code or annotate at the class level and disable creation of accessors by adding configuration key AccessLevel.NONE at field level. The other access levels are PUBLIC, PROTECTED, PACKAGE, and PRIVATE which lets you bend the accessors visibility to your will.

@Setter
@Getter
@AllArgsConstructor
public class Person {
    private int age;
    private final String socialSecurityNumber;
    @Setter(AccessLevel.NONE)
    @Getter(AccessLevel.NONE)
    private boolean calculatedAndOnlyUsedInternally;
    // ...
}

Again combining these annotations freely at the class- and field level is no problem. My advice is to try to be consistent throughout your code base, and chose a strategy that clutters up code the least.

Methods from the Object class

@EqualsAndHashcode

The equals(Object other) and hashCode() methods are a chapter of their own, surrounded by quite a few rules and constraints and, with necessity, lots of opportunity to mess things up. Per default, Lombok includes all non-static, non-transient fields in its generated implementations. There are possibilities to include and exclude fields from the method through annotation configurations.

The Lombok documentation touches the problems around equals and hashCode in inheritance scenarios, and even though there are lots of options to tweak the outcome I would abstain from trying to generate them in these cases and limit the use of @EqualsAndHashCode to simple cases. Of course a good set of unit tests around equals and hashCode will help, but trying to find the right combination of data members would probably be as much work as implementing the methods yourself. Also one can argue that specific needs in equals(..) often belongs to the business logic, and that should be written out! So - keep to simple cases.

@EqualsAndHashCode
public class Person {
    @EqualsAndHashCode.Exclude
    private int age;
    private final String socialSecurityNumber;
    // ...
}

@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Employee extends Person {
    @EqualsAndHashCode.Include
    private String empNumber;
    private double salary;
    // ...
}
@ToString

Like @EqualsAndHashcode this annotation includes all non-static, non-transient fields. The output format cannot be changed and it’s not overly pretty but can be practical for quick access to debug information etc. The toString method of a superclass can be accessed using configuration.

@ToString
public class Person {
    @ToString.Exclude
    private int age;
    private final String socialSecurityNumber;
    // ...
}

@ToString(callSuper = true, onlyExplicitlyIncluded = true)
public class Employee extends Person {
    @ToString.Include
    private String empNumber;
    private double salary;
    // ...
}

Convenience annotations

@Data

Widely used and sometimes misunderstood annotation that includes a number of other Lombok annotations in a so called meta-annotation. The @Data annotation includes

  • @RequiredArgsConstructor
  • @Getter
  • @Setter (on all non-final fields)
  • @EqualsAndHashCode
  • @ToString

This means you may need to include @NoArgsConstructor (in the case of all non-final fields) and possibly @AllArgsConstructor as well in some cases when you define at least one final property. However, this appears to hide the implicit @RequiredArgsConstructor, so you must add this explicitly as well. Personally I tend to use this annotation in very simple and straightforward ad hoc situations, and prefer to include all the contained annotations separately for greater control.

As a sidenote for you JPA users out there: don’t use @Data for your entities as tempting as it may be. Alternatives are available a google lookup away.

@Data
@AllArgsConstructor
@RequiredArgsConstructor
public class Person {
    private int age;
    private final String socialSecurityNumber;
    // ...
}
@Value

Another meta-annotation including:

  • @AllArgsConstructor
  • @Getter
  • @EqualsAndHashCode
  • @ToString
  • @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)

Generates an immutable type by making field final and only providing getters. This annotation is made redundant by the new record Java type, but if you are working in Java versions before 14 it is still a very practical addition for implementing immutable objects.

@Value
public class Person {
    private int age;
    private String socialSecurityNumber;
    // ...
}
@Log

Generates the repetitive logger creation statement. This annotation comes in a couple of flavors for different log libraries. I myself use @Slf4j a lot. You have to provide proper dependencies and a config-file for used logging framework in your project classpath.

@Slf4j
public class ClassWithLogger() {
    public void logIt() {
        log.info("Hello Lombok!");
    }
}
@NonNull

Quick way of getting a null check on constructor argument(s), and this applies both to @AllArgsConstructor and @RequiredArgsConstructor. @NoArgsConstructor doesn’t perform this check.

@Setter
@Getter
public class Person {
    @NonNull
    private Long age;
}

The Builder

@Builder

My personal favorite. Get rid of those pesky constructor calls where half of the arguments are set to null or some other default value, and especially in unit tests where objects need to be set to a predefined state. Also when constructing nested objects the builder pattern is somewhat of a blessing. final properties are no problem either.

@Builder
public class Person {
    private String fullName;
    private int age;
    private final String socialSecurityNumber;
    // ...
}
// Usage:
    var person1 = Person.builder().fullName("Jane Doe").socialSecurityNumber("131313-1313").age(88).build();
    var person2 = Person.builder().fullName("John Wayne").build(); // age = 0 and socialSecurityNumber is NULL
@Builder.Default

You may have noticed that @Builder initializes all untouched properties to null or default for simple types. This behavior can be modified by entering a default value on the property and annotations it properly. This is especially useful on collection type properties where you risk a null pointer if you don’t actively enter an empty collection in your builder usage.

@Builder
public class Person {
    private String fullName;

    @Builder.Default
    private int age = 42;

    @Builder.Default
    private List<String> siblings = new ArrayList<>();
    // ...
}
// Usage:
    var person = Person.builder().fullName("Jane Doe").build();
    var noNPESiblings = person.getSiblings();
@Singular

Annotation that sits on collection type properties. What it does is that it provides the builder with a method for adding single instances to the collection. It also tries to generate a descriptive name; if your collection is named ’persons’ the @Singular will provide a method named ’person’ to the builder. Note that @Builder.Default and @Singular does not work together – choose one.

@Builder
public void  Person() {
    private String fullName;
    @Singular
    private List<String> interests;
}
// Usage:
var person = Person.builder().fullName("Jane Doe").interest("Fencing").interest("Boxing").build();

Usage in other types

Lombok can provide value in other types of objects too, and makes enums neat and minimalistic.

@Getter
@RequiredArgsConstructor
public enum  Direction {
    SOUTH("S"),
    EAST("E"),
    NORTH("N"),
    WEST("W");

    private final String abbreviation;
}

In records you can have the standard ‘getXXX’ method back easily (records name them ‘XXX’ only) by annotating the constructor arguments with @Getter and also a standard factory method using the staticMethod argument in the constructor annotation. Add a @With for easy copying or a @Builder to make construction smooth. (Probably stretching it here, but @Singular works on my machine…)

You’ve probably already thought about it, but don’t forget builders can be passed around for more complex scenarios.

@Builder
public record FamilyMember(
    @Getter @With String firstName,
    @Getter String lastName,
    @Singular List<String> siblings) {
}
// Usage:
var builder = FamilyMember.builder().firstName("Jane").lastName("Doe");
var son = builder.build().withFirstName("John");
var mother = builder.sibling("Joe Doe").sibling("Moe Doe").build();
var mothersFirstName = mother.getFirstName();

Lombok and inheritance

In short: Lombok annotations are discarded by the compiler and therefore doesn’t support inheritance. Generally I would abstain from using other than the very basic stuff in inheritance situations.

Other annotations

This ends the walkthrough of the (from my experience) most useful annotations. Of course there are lots more to explore: the experimental @Accessors or @ExtensionMethod might be fun to try out, for example. Maybe these can give a hint on future features of mainstream Java. Lombok’s new features are battle tested quite thoroughly before they are deemed ready to exit the experimental state which I imagine contributes to general confidence in the library.

Pros

  • Increased developer speed, especially around data centered objects and in early stages of the development cycle
  • Less cluttered code – focus on the important things
  • Stable and battle tested library with a large community
  • Tool support in some (most?) IDEs
  • Possibility to turn all annotation based code into ‘real’ code with the delombok plugin

Cons and a bit of prejudice

  • Not possible to debug (but with easy fix - just implement the method yourself)
  • Your co-developers might be suspicious - do a tech talk to introduce them into the mysteries, and make sure everyone is on board.
  • ”It generates a pile of rubbish” – use only what you need or like (or not at all). Most of the annotations are tweak-able to some extent.
  • ”It makes builds slow” – well, I suspect it has to be a really extensive project if this will give you extra time for another coffee…

Personal conclusion

  • Excellent while building data carriers for persistence or REST services for example. It is so easy changing the properties of the DTO at hand by just adding or removing a field. RAD in its prime?
  • The @Builder is a blessing, in production code but it has a place in unit testing as well. You can easily build partly initialized objects, and the null entries in constructors is reduced to a distant memory.
  • The clean, clutter-free code – for me it reduces cognitive load. It is no fun having accessors for all the fields in a meaty DTO to scroll through.
  • @Data and JPA’s @Entity is not a good match, but there are plenty of workarounds online. While entering JPA territory: be careful with @ToString as well – implement a custom toString method.
  • The Lombok documentation layout – The documentation in itself is good and extensive although the layout raises questions. I don’t know who came up with the idea to make most of it in a really small red font and to ironically include a paragraph (with an even tinier font) named ’Small print’.
  • Give it a try if you haven’t already!

Resources

Bengt-Erik Fröberg

Senior Java Developer at Redpill Linpro

B-E has 20+ years developer experience from both public and private sectors, actually finds unit testing fun and finds the Java ecosystem and the spirit of Open Source communities to be the modern day equivalent of the Renaissance Age.

Time-tracking systems - Timewarrior and ActivityWatch

This is my concluding post in a series of three posts.

  • The first blog post contains my general thoughts on time tracking.
  • The second blog post compares different time-tracking software.
  • When writing this last blog post I’ve been using Timewarrior together with ActivityWatch, Waybar and some home-brewed scripts to track my time as good as I could for a couple of months.

Executive summary

In this blog post I’ll give ... [continue reading]

Time tracking systems - software

Published on May 22, 2025

Time tracking systems - general thoughts

Published on May 13, 2025