Back-end13 minute read

Write Fat-free Java Code with Project Lombok

Java has some idiosyncrasies of its own and design choices that can make it rather verbose. While Java is a mature and performant programming language, developers frequently need to write boilerplate code that bring little or no real value other than complying with some set of constraints and conventions.

In this article, Toptal Freelance Software Engineer Miguel García López shows how Project Lombok can help dramatically reduce the amount of boilerplate code that needs to be written in a Java application.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Java has some idiosyncrasies of its own and design choices that can make it rather verbose. While Java is a mature and performant programming language, developers frequently need to write boilerplate code that bring little or no real value other than complying with some set of constraints and conventions.

In this article, Toptal Freelance Software Engineer Miguel García López shows how Project Lombok can help dramatically reduce the amount of boilerplate code that needs to be written in a Java application.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Miguel García López
Verified Expert in Engineering

Miguel is a passionate software engineer with experience in embedded systems, back-end services, and modern web and mobile applications.

Expertise

PREVIOUSLY AT

Motorola
Share

There are a number of tools and libraries I cannot imagine myself writing Java code without these days. Traditionally, things like Google Guava or Joda Time (at least for the pre Java 8 era) are among the dependencies that I end up throwing into my projects most of the time, regardless of the specific domain at hand.

Lombok surely deserves its place in my POMs or Gradle builds as well, albeit not being a typical library/framework utility. Lombok has been around for quite a while now (first released in 2009) and has matured a lot since. However, I have always felt it deserved more attention—it is an amazing way of dealing with Java’s natural verbosity.

In this post, we will explore what makes Lombok such a handy tool.

Project Lombok

Java has many things going for it beyond just the JVM itself, which is a remarkable piece of software. Java is mature and performant, and the community and ecosystem around it are huge and lively.

However, as a programming language, Java has some idiosyncrasies of its own as well as design choices that can make it rather verbose. Add some constructs and class patterns we Java developers often need to use and we frequently end up with many lines of code that bring little or no real value other than complying with some set of constraints or framework conventions.

Here is where Lombok comes into play. It enables us to drastically reduce the amount of “boilerplate” code we need to write. Lombok’s creators are a couple of very smart guys, and certainly have a taste for humor—you cannot miss this intro they made at a past conference!

Let’s see how Lombok does its magic and some usage examples.

How Lombok Works

Lombok acts as an annotation processor that “adds” code to your classes at compile time. Annotation processing is a feature added to the Java compiler at version 5. The idea is that users can put annotation processors (written by oneself, or via third-party dependencies, like Lombok) into the build classpath. Then, as the compile process is going on, whenever the compiler finds an annotation it sort of asks: “Hey, anybody in the classpath interested in this @Annotation?.” For those processors raising their hands, the compiler then transfers control to them along with compile context for them to, well… process.

Maybe the most common case for annotation processors is to generate new source files or perform some kind of compile-time checks.

Lombok does not really fall into these categories: What it does is modify the compiler data structures used to represent the code; i.e., its abstract syntax tree (AST). By modifying the compiler’s AST, Lombok is indirectly altering the final bytecode generation itself.

This unusual, and rather intrusive, approach has traditionally resulted in Lombok being viewed as somewhat of a hack. While I would myself agree with this characterization to some extent, rather than viewing this in the bad sense of the word, I would view Lombok as a “clever, technically meritorious, and original alternative.”

Still, there are developers who consider it to be a hack and do not use Lombok for this reason. That is understandable, but in my experience, Lombok’s productivity benefits outweigh any of these concerns. I have been happily using it for production projects for many years now.

Before going into details, I’d like to summarize the two reasons I especially value the use of Lombok in my projects:

  1. Lombok helps keep my code clean, concise, and to the point. I find my Lombok annotated classes very expressive and I generally find annotated code to be quite intention-revealing, though not everyone on the internet necessarily agrees.
  2. When I’m starting a project and thinking of a domain model, I tend to begin by writing classes that are very much a work in progress and that I change iteratively as I think further and refine them. In these early stages, Lombok helps me move faster by not needing to move around or transform the boilerplate code it generates for me.

Bean Pattern and Common Object Methods

Many of the Java tools and frameworks we use rely on the Bean Pattern. Java Beans are serializable classes that have a default zero-args constructor (and possibly other versions) and expose their state via getters and setters, typically backed by private fields. We write lots of these, for example when working with JPA or serialization frameworks such as JAXB or Jackson.

Consider this User bean that holds up to five attributes (properties), for which we’d like to have an additional constructor for all attributes, meaningful string representation, and define equality/hashing in terms of its email field:

public class User implements Serializable {

    private String email;

    private String firstName;
    private String lastName;

    private Instant registrationTs;

    private boolean payingCustomer;
    
    // Empty constructor implementation: ~3 lines.
    // Utility constructor for all attributes: ~7 lines.
    // Getters/setters: ~38 lines.
    // equals() and hashCode() as per email: ~23 lines.
    // toString() for all attributes: ~3 lines.

    // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :(
    
}

For brevity here, rather than including the actual implementation of all the methods, I instead just provided comments listing the methods and the number of lines of code that the actual implementations took. That boilerplate code would have totalled more than 90% of the code for this class!

Moreover, if I later wanted to, say, change email to emailAddress or have registrationTs be a Date instead of an Instant then I’d need to devote time (with the help of my IDE for some cases, admittedly) to change things like get/set method names and types, modify my utility constructor, and so on. Again, priceless time for something that brings no practical business value to my code.

Let’s see how Lombok can help here:

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@ToString
@EqualsAndHashCode(of = {"email"})
public class User {

    private String email;

    private String firstName;
    private String lastName;

    private Instant registrationTs;

    private boolean payingCustomer;

}

Voilà! I just added a bunch of lombok.* annotations and achieved just what I wanted. The listing above is exactly all the code I need to write for this. Lombok is hooking into my compiler process and generated everything for me (see the screenshot below of my IDE).

IDE Screenshot

As you notice, the NetBeans inspector (and this will happen regardless of IDE) does detect the compiled class bytecode, including the additions Lombok brought into the process. What happened here is quite straightforward:

  • Using @Getter and @Setter I instructed Lombok to generate getters and setters for all attributes. This is because I used the annotations at a class level. If I wanted to selectively specify what to generate for which attributes, I could have annotated the fields themselves.
  • Thanks to @NoArgsConstructor and @AllArgsConstructor, I got a default empty constructor for my class as well as an additional one for all the attributes.
  • The @ToString annotation auto-generates a handy toString() method, showing by default all class attributes prefixed by their name.
  • Finally, to have the pair of equals() and hashCode() methods defined in terms of the email field I used @EqualsAndHashCode and parameterized it with the list of relevant fields (just the email in this case).

Customizing Lombok Annotations

Let’s now use some Lombok customizations following this same example:

  • I’d like to lower the visibility of the default constructor. Because I only need it for bean compliancy reasons, I expect consumers of the class to only call the constructor that takes all fields. To enforce this, I am customizing the generated constructor with AccessLevel.PACKAGE.
  • I want to ensure that my fields never get assigned null values, neither via the constructor nor via the setter methods. Annotating class attributes with @NonNull is enough; Lombok will generate null checks throwing NullPointerException when appropriate in the constructor and setter methods.
  • I’ll add a password attribute, but do not want it shown when calling toString() for security reasons. This is accomplished via the excludes argument of @ToString.
  • I’m OK exposing state publicly via getters, but would prefer to restrict outside mutability. So I am leaving @Getter as is, but again using AccessLevel.PROTECTED for @Setter.
  • Perhaps I would like to force some constraint on the email field so that, if it gets modified, some kind of check is run. For this, I just implement the setEmail() method myself. Lombok will just omit generation for a method that already exists.

This is how the User class will then look:

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter(AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor
@ToString(exclude = {"password"})
@EqualsAndHashCode(of = {"email"})
public class User {

    private @NonNull String email;

    private @NonNull byte[] password;

    private @NonNull String firstName;
    private @NonNull String lastName;

    private @NonNull Instant registrationTs;

    private boolean payingCustomer;

    protected void setEmail(String email) {
        // Check for null (=> NullPointerException) 
        // and valid email code (=> IllegalArgumentException)
        this.email = email;
    } 
    
}

Note that, for some annotations, we are specifying class attributes as plain strings. Not a problem, because Lombok will throw a compile error if we, for example, mistype or refer to a non-existing field. With Lombok, we’re safe.

Also, just like for the setEmail() method, Lombok will just be OK and not generate anything for a method the programmer has already implemented. This applies to all methods and constructors.

Immutable Data Structures

Another use case where Lombok excels is when creating immutable data structures. These are usually referred to as “value types.” Some languages have built-in support for these, and there’s even a proposal for incorporating this into future Java versions.

Suppose we want to model a response to a user login action. This is the kind of object we’d want to just instantiate and return to other layers of the application (for example, to be JSON serialized as the body of an HTTP response). Such a LoginResponse would not need to be mutable at all and Lombok can help describe this succinctly. Sure, there are many other use cases for immutable data structures (they’re multithreading- and cache-friendly, among other qualities), but let’s stick to this simple example:

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.experimental.Wither;

@Getter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
public final class LoginResponse {

    private final long userId;
    
    private final @NonNull String authToken;
    
    private final @NonNull Instant loginTs;

    @Wither
    private final @NonNull Instant tokenExpiryTs;
    
}

Worth noting here:

  • A @RequiredArgsConstructor annotation has been introduced. Aptly named, what it does is generate a constructor for all final fields that have not already been initialized.
  • In cases where we want to reuse a previously issued LoginResonse (imagine, for example, a “refresh token” operation), we certainly don’t want to modify our existing instance, but rather, we want to generate a new one based on it. See how the @Wither annotation helps us here: It tells Lombok to generate a withTokenExpiryTs(Instant tokenExpiryTs) method that creates a new instance of LoginResponse having all the with’ed instance values, except the new one we’re specifying. Would you like this behavior for all fields? Just add @Wither to the class declaration instead.

@Data and @Value

Both use cases discussed so far are so common that Lombok ships a couple of annotations to make them even shorter: Annotating a class with @Data will trigger Lombok to behave just as if it had been annotated with @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor. Likewise, using @Value will turn your class into an immutable (and final) one, also again as if it had been annotated with the list above.

Builder Pattern

Going back to our User example, if we want to create a new instance, we’ll need to use a constructor with up to six arguments. This is already a rather large number, which will get even worse if we further add attributes to the class. Also suppose we’d want to set some default values for the lastName and payingCustomer fields.

Lombok implements a very powerful @Builder feature, allowing us to use a Builder Pattern to create new instances. Let’s add it to our User class:

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter(AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor
@ToString(exclude = {"password"})
@EqualsAndHashCode(of = {"email"})
@Builder
public class User {

    private @NonNull String email;

    private @NonNull byte[] password;

    private @NonNull String firstName;
    private @NonNull String lastName = "";

    private @NonNull Instant registrationTs;

    private boolean payingCustomer = false;

}

Now we are able to fluently create new users like this:

User user = User
        .builder()
            .email("miguel.garcia@toptal.com")
            .password("secret".getBytes(StandardCharsets.UTF_8))
            .firstName("Miguel")
            .registrationTs(Instant.now())
        .build();

It’s easy to imagine how convenient this construct becomes as our classes grow.

Delegation/Composition

If you want to follow the very sane rule of “favor composition over inheritance” that’s something Java does not really help with, verbosity wise. If you want to compose objects, you’d typically need to write delegating method calls all over the place.

Lombok proposes a solution for this via @Delegate. Let’s take a look at an example.

Imagine that we want to introduce a new concept of ContactInformation. This is some information our User has and we may want other classes to have too. We can then model this via an interface like this:

public interface HasContactInformation {

    String getEmail();
    String getFirstName();
    String getLastName();

}

We would then introduce a new ContactInformation class using Lombok:

import lombok.Data;

@Data
public class ContactInformation implements HasContactInformation {

    private String email;

    private String firstName;
    private String lastName;

}

And finally, we could refactor User to compose with ContactInformation and use Lombok to generate all the required delegating calls to match the interface contract:

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Delegate;

@Getter
@Setter(AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor
@ToString(exclude = {"password"})
@EqualsAndHashCode(of = {"contactInformation"})
public class User implements HasContactInformation {

    @Getter(AccessLevel.NONE)
    @Delegate(types = {HasContactInformation.class})
    private final ContactInformation contactInformation = new ContactInformation();

    private @NonNull byte[] password;

    private @NonNull Instant registrationTs;

    private boolean payingCustomer = false;

}

Note how I did not need to write implementations for the methods of HasContactInformation: this is something we’re telling Lombok to do, delegating calls to our ContactInformation instance.

Also, because I do not want the delegated instance to be accessible from outside, I am customizing it with a @Getter(AccessLevel.NONE), effectively preventing getter generation for it.

Checked Exceptions

As we all know, Java differentiates between checked and unchecked exceptions. This is a traditional source for controversy and criticism to the language as exception handling sometimes gets too much in our way as a result, especially when dealing with APIs designed to throw checked exceptions and therefore forcing us developers to either catch them or declare our methods to throw them.

Consider this example:

public class UserService {

    public URL buildUsersApiUrl() {
        try {
            return new URL("https://apiserver.com/users");
        } catch (MalformedURLException ex) {
            // Malformed? Really?
            throw new RuntimeException(ex);
        }
    }

}

This is such a common pattern: We certainly know our URL is well formed, yet—because the URL constructor throws a checked exception—we are either forced to catch it or declare our method to throw it and put out callers in the same situation. Wrapping these checked exceptions inside a RuntimeException is a very extended practice. And this gets even worse if the number of checked exceptions we need to deal with grows as we code.

So this is exactly what Lombok’s @SneakyThrows is for, it’ll wrap any checked exceptions subject to be thrown in our method into a unchecked one and free us from the hassle:

import lombok.SneakyThrows;

public class UserService {

    @SneakyThrows
    public URL buildUsersApiUrl() {
        return new URL("https://apiserver.com/users");
    }

}

Logging

How often do you add logger instances to your classes like this? (SLF4J sample)

private static final Logger LOG = LoggerFactory.getLogger(UserService.class);

I’m going to guess quite a bit. Knowing this, the creators of Lombok implemented an annotation that creates a logger instance with a customizable name (defaults to log), supporting the most common logging frameworks on the Java platform. Just like this (again, SLF4J based):

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UserService {

    @SneakyThrows
    public URL buildUsersApiUrl() {
        log.debug("Building users API URL");
        return new URL("https://apiserver.com/users");
    }

}

Annotating Generated Code

If we use Lombok to generate code, it may seem we’d lose the ability to annotate those methods since we’re not actually writing them. But this is not really true. Rather, Lombok allows us to tell it how we’d want generated code to be annotated, using a somewhat peculiar notation though, truth be told.

Consider this example, targeting the use of a dependency injection framework: We have a UserService class that uses constructor injection to get the references to a UserRepository and UserApiClient.

package com.mgl.toptal.lombok;

import javax.inject.Inject;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class UserService {

    private final UserRepository userRepository;
    private final UserApiClient userApiClient;

    // Instead of:
    // 
    // @Inject
    // public UserService(UserRepository userRepository,
    //                    UserApiClient userApiClient) {
    //     this.userRepository = userRepository;
    //     this.userApiClient = userApiClient;
    // }

}

The sample above shows how to annotate a generated constructor. Lombok allows us to do the same thing for generated methods and parameters as well.

Learning More

The Lombok usage explained in this post focuses on those features that I have personally found to be most useful over the years. However, there are many other features and customizations, available.

Lombok’s documentation is very informative and thorough. They have dedicated pages for every single feature (annotation) with very detailed explanations and examples. If you find this post interesting, I encourage you to dive deeper into lombok and its documentation to find out more.

The project site documents how to use Lombok in several different programming environments. In short, most popular IDEs (Eclipse, NetBeans and IntelliJ) are supported. I myself regularly switch from one to another on a per-project basis and use Lombok on all of them flawlessly.

Delombok!

Delombok is part of the “Lombok toolchain” and can come in very handy. What it does is basically generate the Java source code for your Lombok annotated code, performing the same operations the Lombok generated bytecode does.

This is a great option for people considering adopting Lombok but not quite sure yet. You may freely start using it and there’ll be no “vendor lock-in.” In case you or your team later regret the choice, you can always use delombok to generate the corresponding source code which you can then use without any remaining dependency on Lombok.

Delombok is also a great tool to learn exactly what Lombok will be doing. There are very easy ways to plug it into your build process.

Alternatives

There are many tools within the Java world that make a similar usage of annotation processors to enrich or modify your code at compile time, such as Immutables or Google Auto Value. These (and others, for sure!) overlap with Lombok feature-wise. I particularly like the Immutables approach a lot and have also used it in some projects.

It’s also worth noting that there are other great tools providing similar features for “bytecode enhancing”, such as Byte Buddy or Javassist. These typically work at runtime though, and comprise a world of their own beyond the scope of this post.

Concise Java

There are a number of modern JVM targeted languages that provide more idiomatic—or even language level—design approaches helping address some of the same issues. Surely Groovy, Scala, and Kotlin are nice examples. But if you are working on a Java-only project, then Lombok is a nice tool to help your programs be more concise, expressive, and maintainable.

Hire a Toptal expert on this topic.
Hire Now
Miguel García López

Miguel García López

Verified Expert in Engineering

Murcia, Spain

Member since January 2, 2017

About the author

Miguel is a passionate software engineer with experience in embedded systems, back-end services, and modern web and mobile applications.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

Motorola

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.