Here at Grubhub, we use Java for most of our backend programming. Java is a battle-tested language that has proven its speed and reliability over the last 20 years. While we have been using Java for many years, recently, it has started to show its age.

Although Java is one of the most popular JVM languages, it’s not the only one. In the last few years, it’s faced several challengers, such as Scala, Clojure, and Kotlin, that provide new functionality and streamlined language features. In short, they let you do more with less verbose code.

This innovation in the JVM ecosystem is exciting to see. More competition has meant that Java has been forced to change to stay competitive. The new six month release schedule and several JEPs (JDK enhancement proposals) since Java 8 (Valhalla, Local-Variable Type Inference, Loom) are proof that Java will remain a competitive language for years to come.

However, the size and scale of the Java language means that development moves slower than we would like, not to mention Java’s strong desire to maintain backwards compatibility at all costs. With any software engineering effort, features need to be prioritized, so the features we want may take a long time, if they make it into Java at all. In the meantime, Grubhub leverages Project Lombok to get a streamlined and improved Java now. Lombok is a compiler plugin that adds new “keywords” to Java and turns annotations into Java code, reducing engineering busywork and providing some additional functionality.

Setting up Lombok

Grubhub is always looking to improve our software lifecycle, but every new tool and processes has a cost that must be considered before adopting. Luckily, adding Lombok is as easy as adding a couple lines to a gradle file.

Lombok is a compiler plugin, as it converts the annotations in your source code into Java statements before the compiler processes them — the lombok dependency does not need to present at runtime, so adding Lombok will not increase the size of build artifacts. So you’ll need to download Lombok and add it to your build tool. To set up Lombok with Gradle (it works with Maven too), add this block to the build.gradle file:

Since Lombok is a compiler plugin, the source code we write for it is actually not valid Java. So you’ll also need to install a plugin for the IDE you are using. Fortunately, Lombok supports all the major Java IDEs. Without the plugin, the IDE has no idea how to parse the code. The IDE integration is seamless. Features such as “show usages” and “go to implementation” continue to work as expected, taking you to the relevant field/class.

Lombok in action

The best way to learn about Lombok is to see it in action. Let’s dig into some examples on how to apply Lombok to common aspects of a Java application.

Spice Up A POJO

We use plain old Java objects (POJOs) to separate data from processing, making our code easier to read and simplifying network payloads.. A simple POJO has some private fields and corresponding getters and setters. They get the job done but only after writing a lot of boilerplate code.

Lombok helps make POJOs more useful, flexible, and structured without writing much additional code. With Lombok, we can simplify the most basic POJO with the @Data annotation:

The @Data annotation is really just a convenience annotation that applies multiple Lombok annotations.

@ToString generates an implementation for the toString() method which consists of a “pretty print” version of the object containing the class name and each field and its value.

generates an implementation for the method which consists of a “pretty print” version of the object containing the class name and each field and its value. @EqualsAndHashCode generates implementations of the equals and hashCode methods that, by default, uses all non-static, non-transient fields, but is configurable.

generates implementations of the and methods that, by default, uses all non-static, non-transient fields, but is configurable. @Getter / @Setter generate getter and setter methods for private fields.

/ generate getter and setter methods for private fields. @RequiredArgsConstructor generates a constructor with the required arguments, where a required arguments are final fields and fields annotated with @NonNull (more on that later).

That one annotation covers many common use cases simply and elegantly. But a POJO isn’t always enough. A @Data class is entirely mutable, which when abused, can increase complexity in an application and limit concurrency usage, both of which hurt the longevity of an application.

Lombok has just the fix. Let’s revisit our User class,make it immutable, and add a few other useful Lombok annotations.

All it takes is the @Value annotation. @Value is similar to @Data except, all fields are made private and final by default and setters are not generated. These qualities make @Value objects effectively immutable. As the fields are all final, there is not a no argument constructor. Instead Lombok uses @AllArgsConstructor to generate an all arguments constructor. This results in a fully-functioning, effectively-immutable object.

But being immutable isn’t very useful if you can only create an object using an all args constructor. As Effective Java by Joshua Bloch explains, builders should be used when faced with many constructor parameters. That is where Lombok’s @Builder steps in, automatically generating a builder inner class:

Using the Lombok-generated builder makes it easy to create objects with many arguments and to add new fields in the future. The static builder method returns a builder instance to set all the properties of the object. Once set, invoke build() on the builder to return an instance.

The @NonNull annotation can be used to assert that those fields are not null when the object is instantiated, throwing a NullPointerException when null. Notice how the avatar field is annotated with @NonNull but it is not set. That is because the @Builder.Default annotation indicates to use “default.png” by default.

Also notice how the builder is using favoriteFood , the singular name of the property on our object. When the @Singular annotation is placed on a collection property, Lombok creates special builder methods to individually add items to that collection, rather than adding the entire collection at once. This is particularly nice for tests as creating small collections in Java is not concise.

Finally, the toBuilder = true setting adds an instance method toBuilder() that creates a builder object populated with all the values of that instance. This enables an easy way to create a new instance prepopulated with all the values from the original instance and change just the fields needed. This is particularly useful for @Value classes because the fields are immutable.

A few annotations let you further configure specialized setter functions. @Wither creates “ withX ” methods for each property that accept a value and returns a cloned of the instance with the one field value updated. @Accessors lets you configure automatically created setters. By default, it allows setters to be chained, where like a builder, this is returned rather than void. It also has a parameter, fluent=true , which drops the “get” and “set” prefix convention on getters and setters. This can be a useful replacement for @Builder if the use case requires more customization.

If the Lombok implementation does not fit your use case (and you have looked at the annotation’s modifiers), then you can always just write your own implementation by hand. For example, if you had a @Data class but a single getter needed custom logic, simply implement that getter. Lombok will see that an implementation is already supplied and will not overwrite it with the autogenerated implementation.

With just a few simple annotations, the initial User POJO has gained so many rich features that make it easier to use without putting much burden on us engineers or increasing the time or cost to develop.

Remove component boilerplate code

Lombok isn’t just useful in POJOs — it can be applied at any layer of an application. The following usages of Lombok are particularly useful in the component classes of an app, such as controllers, services, and DAOs (data access objects).

Logging is a baseline requirement for every piece of software, serving as a critical investigation tool. Any class that is doing meaningful work should be logging information. As logging is a cross-cutting concern, declaring a private static final logger in every class becomes instant boilerplate. Lombok simplifies this boilerplate into one annotation that automatically defines and instantiates a logger with the right class name. There are a handful of different annotations depending on the logging framework you are using.

With a logger declared, next let’s add our dependencies:

The @FieldDefaults annotation adds the final and private modifiers to all of the fields. The @RequiredArgsConstructor creates a constructor that accepts and sets a UserDao instance. The @NonNull annotation adds a check in the constructor and throws a NullPointerException if the UserDao instance is null.

But wait, there’s more!

There are so many ways to use Lombok. The above two sections focused on specific use cases, but Lombok can make development easier in many areas. Here are a couple small examples that show off how to better leverage Lombok effectively.

Although Java 9 introduces the var keyword, a var can still be reassigned. Lombok provides a val keyword which picks up where var leaves off, providing local final type inferred variables.

Some classes just have pure static functions and are never meant to be initialized. Declaring a private constructor that throws an exception is one way prevent it from getting instantiated. Lombok has codified that pattern in its @UtilityClass annotation which creates a private constructor that throws an exception, makes the class final, and makes all methods static.

A common critique of Java is the verbosity created by throwing checked exceptions. Lombok has an annotation to remove the need for those pesky throws keywords: @SneakyThrows . As you might expect, the implementation is quite sneaky. It does not swallow or even wrap exceptions into a RuntimeException . Instead, it relies on the fact that at runtime, the JVM does not check for the consistency of checked exceptions. Only javac does this. So Lombok uses bytecode transformations to opt out of this check at compile time. As a result, this results in runable code.

Side by side comparison

Nothing beats seeing how much code Lombok saves than doing a side by side comparison. The IDE plugin offers a “de-lombok” function that converts most Lombok annotations into the approximate native Java code (the @NonNull annotation not converted). Any IDE with the Lombok plugin installed lets you convert most annotations into native Java code (and back again). Let’s return to our User class from above.

The Lombok class is just 13 simple, readable, descriptive lines of code. But after running de-lombok, the class is transformed into over a hundred lines boilerplate that no one wants to see, but everyone wants!

We can do the same for the UserService class from above.

Will result in approximately this Java code.

Measuring the impact

Grubhub has over a hundred services running to accomplish the needs of the business. We took one of these services and ran the “de-lombok” functionality of the Lombok IntelliJ plugin to see how many lines of code were saved by using Lombok. The result was a change to approximately 180 files, resulting in about 18,000 additional lines of code and 800 deletions of Lombok usages. That is 18,000 lines of auto-generated, standardized, and battle-tested lines of code! On average, each line of Lombok code is saving 23 lines of Java code. With an impact like that, it is hard to imagine using Java without Lombok.

Summary

Lombok has been an excellent way to excite engineers with the appearance of new language features without requiring much effort across the organization. It is certainly easier to apply a plugin to a project than to train all engineers on a new language and port over existing code. Lombok may not have everything, but it certainly provides enough out of the box to have a noticeable impact on the engineering experience.

One other benefit of Lombok is that it keeps our codebases consistent. With over a hundred different services and distributed teams across the world, keeping our codebases in alignment makes it easier to scale teams and reduce the burden of context switching when starting a new project. Lombok is relevant for any version of Java since 6, so we can count on it being available in all projects.