Project Lombok: Fun with Builders!

Benedikt Jerat
Digital Frontiers — Das Blog
5 min readOct 28, 2022
Photo by Brad West on Unsplash

Builders in Lombok are amazing. They’re being automatically produced by just adding an annotation above your class and there you go. No need to write tedious boilerplate code. Most of the time, you can use them without further thinking and adaptations. However, sometimes we have special requirements that don’t fit into the standard. Then it’s time for some tinkering to support these sophisticated patterns with Lombok builders!

Today, I brought three of these patterns to solve different use cases:

  1. Custom build-method (e.g. for adding custom validation)
  2. Custom logic for setting key/value pairs in maps (e.g. for convenient setters and transformation logic)
  3. Prevent null access for optional nested structures

These are patterns that I need now and then in projects. May they be a helpful addition to your Lombok repertoire!

Pattern 1: Custom build-method

Normally, there’s no need to override the auto-generated build method provided by Lombok builders. However, in case you need some logic before or after the build method, you cannot just override it directly. Well, in fact you could, but then you would be required to rewrite the whole build method, including instantiating the resulting class from the internal builder fields:

This would work. However, this makes your builder unstable to changes to the base class. If the order of your fields change, or you add or remove a field, you would need to update your build method. But wasn’t this exactly the case, why you added the @Builder annotation above your class? So, Lombok is handling such stuff for you without you having to rewrite those invocations?

Fortunately, there’s a solution for this issue. However, to make this work, you are required to override the builder() method as well:

We let the CertificateDefinitionBuilder do its thing. We’re just defining a custom builder class CustomCertificateDefinitionBuilder and let it inherit from the generated builder class. Its build method can be overriden without any hassle, so you can add any logic before or after the actual build method. You just need to make sure to override the builder method as well, so clients of your class receive the adapted version of your builder class transparently:

Here you can see the builder invocation, with no trace of any internal adjustments, like our added validation, noticeable.

If we would omit the asset identifier, the validation logic would throw an exception as desired:

Pattern 2: Custom logic for setting key/value pairs in maps

This next pattern comes up, whenever I work with semi-structured maps or lists. For example, you have a map of string-to-string pairs. Some pairs may be known upfront, so you can predefine them as methods for your clients. Maybe some values require a complex transformation. In that case, you can provide a setter method inside your builder that takes this heavy burden from the user. Whatever the use case, it all starts with a @Singular annotation above your list or map:

This enables you to use chains of single element inserts to your collection:

In that case, secrets is a map of string-to-string pairs. By adding the @Singular annotation, the secret method is generated, so you can add pairs easily. Now let’s predefine a method keystoreSecret, so we can let the users know that this key exists and is potentially being handled differently:

For this use case, it’s enough to just define the default builder class and add whatever functionality is needed. The rest of the builder class will be generated by Lombok behind the scenes.

Now, we can invoke a new method keystoreSecret, which handles adding this pair to the map transparently as well:

We could add more logic to the method, like transforming the keystore value from a byte array instead:

In any case, we can make life easier for users of our builder by just thinking about the usage at design-level.

Pattern 3: Preventing null access for optional nested structures

This one has a very special place in my collection. Maybe you will never encounter that demand in your whole career. But in case you do, have fun with this solution!

Suppose you have a class, which is also serialized and deserialized into a different format, like JSON. And suppose you have a map-like structure inside your class with well-known key value pairs. However, the sender does not always provide this field, so you would be required to check for null references for each access to the map-like structure. Wouldn’t that be very tedious and error-prone? I thought so, too, therefore I tinkered around with different approaches. Only one of them met my requirements.

But first, let me give you an exemplary implementation of such a class:

This implementation is fine up to the point, where we don’t care for every pair in the additionalProperties field and the field is always provided by the sender. In my case, we were consuming this event as a JSON string. As a tolerant reader, we only want to read as much as necessary. Back then, we only cared about three well-known pairs of the map. So, the first logical iteration was to add a new type, removing the necessity to declare the additionalProperties as a map:

This is getting deserialized without any issue from a JSON string:

{
"namespace": "me",
"resourceName": "also-me",
"additionalProperties": {
"renewBefore": 1
}
}
CertificateEvent(
namespace=me,
resourceName=also-me,
additionalProperties=CertificateEvent.Metadata(
notAfter=null,
renewBefore=1,
errorMessage=null
)
)

But what happens when the sender is omitting the additionalProperties?

{
"namespace": "me",
"resourceName": "also-me"
}
CertificateEvent(
namespace=me,
resourceName=also-me,
additionalProperties=null
)

Well, if you would access the field without checking now, you would be warmly welcomed by a NullPointerException. To solve this issue, the only satisfying solution to me was adding a @Builder.Default annotation to initialize the field, if no value is given:

Now, whenever the field is not provided in the JSON string, the annotation ensures that at least the map-like structure cannot ever be null:

{
"namespace": "me",
"resourceName": "also-me"
}
CertificateEvent(
namespace=me,
resourceName=also-me,
additionalProperties=CertificateEvent.Metadata(
notAfter=null,
renewBefore=null,
errorMessage=null
)
)

Of course, the fields inside our Metadata class can be null nevertheless, but that’s less of an issue. In fact, if Java would have a safe call operator ?., like Kotlin does, this whole use case would be obsolete. But I guess we’ll have to live with that for now.

That’s it and thanks for reading! Maybe you want to share your sophisticated use cases, requiring you to adapt the Lombok builder yourself. Or maybe you found a more elegant to solution to one the use cases I provided. In any case, feel free to comment or message me, of course also when you have questions or suggestions.

And lastly: You might be interested in the other posts published in the Digital Frontiers blog, announced on our Twitter account. Have fun exploring!

--

--