Skip to content

Instantly share code, notes, and snippets.

@slobodator
Last active January 13, 2025 11:16

Do not Use Lombok Builder

TL; DR. Don’t use Lombok Builder.

The only exception is a “fluent filter”.

Q: Hey, Lombok is an awesome tool. Why are you against it?

A: I didn’t say I’m against Lombok. I like it too. But it has a lot of features written by angels and devils. Lombok Builder is definitely from the dark side.

Q: Are you against the Builder Pattern? It’s the well-known OOP pattern!

A: I’m fine with the Builder Pattern. But Lombok Builder does not provide it in the right way.

Q: How to distinguish a good builder and a bad builder?

A: It’s easy. The good one should return a fully initialised object in any case. Assume we provide a carBuilder for a rental agency. If a customer just wants to drive and doesn't have any special requests, the carBuilder.build() should provide them some car that is ready to go, i.e. the car.drive() does its job and doesn't throw any exception.

Another customer may call

carBuilder
    .ofType(CarType.ECONOMY)
    .withTransmission(Transmission.AUTO)
    .withFeature(Feature.CHILD_SEAT)
    .withFeature(Feature.GPS_NAVIGATOR)
    .build();

... and that should be also fine.

Now let’s have a deeper look at Lombok Builder. Assume we have

@Builder
public class MyClass {
    private String str;
}

and invoke MyClass obj = MyClass.builder().build();

Would the str be initialised? No, it would be null!

Moreover, there is a caveat with default values. Assume we add

@Builder
class MyClass {
    private String str = "xyz";
}

but if we call the builder again the str would still be null. Why? Because @Builder.Default was forgotten.

Also, @Builder conflicts with @NoArgConstructor and requires @AllArgsConstructor in this case.

But these are technical details. The main thing is that Lombok Builder doesn't provide fully initialised object and thus is the bad builder. Don’t use it.

Q: So how should I initialise the objects?

A: There is a clear answer at OOP — use the constructor(s).

Q: But I have a class with 86 fields. Does it mean I have to create the constructor with 86 parameters? I would prefer to use the builder and set only necessary ones.

A: No. The class should have not more than 5-8 fields. If it has more than that consider to extract subclasses. Also, necessary ones sound weird. It doesn't mean that all fields have to be assigned but the object should be fully initialised from the business perspective.

Q: But it is nonce! My classes are huge, much more that 5-8 fields! Have you ever seen real projects or just tutorials?

A: Yes, I have seen. The answer is using composition. Any complex object like a car, a human or even Universum may be split into 5-8 parts, each of them would be split to sub-parties and so on. Any complex system is built like that. Indeed, if all fields are stored in “a flat set”, we need “a builder” to assign them.

Q: I have a class with a few fields, but they are of the same type i.e.

class Address {
    private String country;
    private String city;
    private String streetLine1;
    private String streetLine2;
}

so its constructor signature is new Address(String, String, String, String) and I’m afraid to mix them up.

A: This is a fair point. Modern JVM languages such as Groovy and Kotlin offer syntax sugar for that and allow to initialise it like a map at any order,

i.e. new Address(city: "Abu Dhabi", country: "UAE",...

...but pure Java doesn't.

IDEA also gives you a hint. Be tidy and still use the constructor.

Q: I follow the rule that the entities should not escape the transaction boundaries and convert them to the DTOs...

A: Use MapStruct for that.

Q: … but I don’t want to inject the dependency for a single mapping. Could I use the builder for that, i.e.

Dto.builder()
    .withField(...)
    .build();

...?

A: I would still suggest to use MapStruct. It has a killer feature that checks that all DTO’s fields are assigned (or explicitly ignored). If you don’t want to use MapStruct anyhow, build the DTO with its constructor as well. At the previous article I used a builder for mapping but missed the field createdAt.

Q: I can’t still get what is wrong of using Lombok Builder either for building domain objects or their responses. I did that for years without any issues.

A: The issue is that in case of adding a new mandatory property it could be un-initialized.

Q: But what is wrong to find all builder usages and fix them? IDEA or any IDE will assist you with that.

A: It is fragile and unsafe. The only correct way is using constructors. When you’re adding a new property, the old code won’t be compiled unless you review and fix it. You can’t miss the new property this way.

Q: I’m writing tests for the controller. It accepts some request (also a DTO). How should I set only necessary fields at it? The client doesn't guarantee that the request is consistent. Lombok Builder seems to fit for that...

A: Yes, this is the only valid case. By its definition there is no guarantee of the request consistency. So, using inconsistent Lombok Builder to build an inconsistent request is acceptable.

Another valid approach of using Lombok Builder is building “a fluent filter”. Assume, we are filtering a car from a car repository/microservice. Actually, all our criteria are optional, so, it is ok to set something like

CarCriteriaFilter.builder()
   .manufacturer("Lamborgini")
   .priceTo(Money.of(500_000, Currency.USD))
   .yearFrom(2020)
   .build();

It is ok not to set the priceFrom and yearFrom. Actually, it is ok to set no criteria at all like

CarCriteriaFilter.builder()
  .build();

So, in this case the CarCriteriaBuilder satisfies the definition of the good builder as it is a kind of consistent at its any state.

Conclusions

Do not use Lombok Builder for domain classes.

Use constructors instead.

Compose huge classes from smaller subclasses.

The only acceptable cases for Lombok Builder are possible inconsistent requests and fluent filters.

@Chrimle
Copy link

Chrimle commented Jan 10, 2025

Albeit lengthy, this was not only a good walkthrough for the drawbacks of Lombok - but more importantly how builders should be designed. It should clearly reflect required/optional properties, ensure that the resulting entity is valid once built - and most importantly, it should be intuitive and easy for others to use.

It would benefit from some simple reformatting, but overall really good 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment